13.07.2015 Aufrufe

Projekt Froderon: Zur weiteren Entwicklung der ...

Projekt Froderon: Zur weiteren Entwicklung der ...

Projekt Froderon: Zur weiteren Entwicklung der ...

MEHR ANZEIGEN
WENIGER ANZEIGEN

Sie wollen auch ein ePaper? Erhöhen Sie die Reichweite Ihrer Titel.

YUMPU macht aus Druck-PDFs automatisch weboptimierte ePaper, die Google liebt.

<strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong>:<strong>Zur</strong> <strong>weiteren</strong> <strong>Entwicklung</strong> <strong>der</strong>Programmiersprache Oberon-2Eine Analyse möglicher Spracherweiterungen und <strong>der</strong>enIntegration in einen bestehenden CompilerDiplomarbeitPeter FröhlichFachhochschule MünchenFachbereich Informatik/MathematikWintersemester 1996/1997Betreuer:Zweitbetreuer:Prof. Dr. Klaus KöhlerProf. Dr. Ulrich Möncke


iiiWhen will this guy fi-Abstracts, Abstracts, Abstracts!nally get concrete?— Herkunft unbekanntAbstractThis thesis discusses extensions to the programming language Oberon-2, asimpleyetpowerful language in the tradition of Pascal and Modula-2, supporting object-orientationin a minimalistic way.We present a new approach to object-orientation that separates subclassing fromsubtyping, allowing multiple subtyping and single subclassing to coexist and benefitfrom each other. We also discuss constructs for program specification based on axiomaticsemantics, protected visibility for classes, constant parameter passing, and side-effect freefunctions. Several other problems of the Oberon-2 language are identified, too, togetherwith proposed solutions.To integrate the extensions with Oberon-2 we define the experimental programminglanguage <strong>Fro<strong>der</strong>on</strong>-1 as a true descendant of Oberon-2, and show the complexity of ourextensions by developing a <strong>Fro<strong>der</strong>on</strong>-1 compiler based on the well-known OP2 compilerfor Oberon-2.ZusammenfassungDiese Arbeit diskutiert Erweiterungen <strong>der</strong> Programmiersprache Oberon-2, einer einfachen,aber mächtigen Sprache in <strong>der</strong> Tradition von Pascal und Modula-2, dieObjektorientierungin minimalistischer Weise unterstützt.Wir stellen einen neuen Zugang zur Objektorientierung vor, <strong>der</strong> zwischen subclassingund subtyping unterscheidet und es so möglich macht, daß multiple subtyping undsingle subclassing nebeneinan<strong>der</strong> existieren und von gegenseitigem Nutzen sein können.Außerdem diskutieren wir Konstrukte für die Spezifikation von Programmen auf <strong>der</strong>Basis axiomatischer Semantik, einen geschützten Sichtbarkeitsbereich für Klassen, konstanteProzedurparameter und seiteneffektfreie Funktionen. Einige an<strong>der</strong>e Probleme<strong>der</strong> Programmiersprache Oberon-2 werden ebenfalls angesprochen und Lösungen für sieskizziert.Um die Erweiterungen in Oberon-2 einzubringen, definieren wir die experimentelleProgrammiersprache <strong>Fro<strong>der</strong>on</strong>-1 als echten Erben von Oberon-2 und zeigen anhand<strong>der</strong> <strong>Entwicklung</strong> eines <strong>Fro<strong>der</strong>on</strong>-1-Compilers, <strong>der</strong> auf dem bekannten OP2 Compiler fürOberon-2 basiert, die Komplexität unserer Erweiterungen auf.


vDanksagungenWährend <strong>der</strong> Erstellung dieser Arbeit haben mir viele Menschen direkt o<strong>der</strong> indirektgeholfen, allen voran Prof. Dr. Klaus Köhler, dem ich hier sowohl für seine gute(und geduldige) Betreuung <strong>der</strong> Arbeit als auch für seinen Einsatz in Vorlesungen undÜbungen an <strong>der</strong> Fachhochschule München herzlich danken möchte. Er hat durch seineVorlesungen erst mein Interesse an höheren Programmiersprachen im allgemeinen undan objektorientierer Programmierung im beson<strong>der</strong>en geweckt. Auch Prof. Dr. UlrichMöncke, <strong>der</strong> die Zweitbetreuung dieser Arbeit übernommen hat, gebührt mein Dank.<strong>Zur</strong> <strong>Entwicklung</strong> von <strong>Fro<strong>der</strong>on</strong>-1 haben auch Frank Copeland, Lars Düning(TU Braunschweig), Tim Teulings (Uni Bonn), Prof. Dr. Hanspeter Mössenböck(Uni Linz), Stefan Ludwig (ETH Zürich), und Dr. Robert Griesemerdurch (teils kontroverse) Diskussionen beigetragen; außerdem natürlich viele Teilnehmer<strong>der</strong> fro<strong>der</strong>on Mailingliste und <strong>der</strong> newsgroup comp.lang.oberon. Ein beson<strong>der</strong>erDank gebührt Günter Dotzel (ModulaWare France) für die Erfindung des Namens<strong>Fro<strong>der</strong>on</strong>, auch wenn dieser einem gänzlich an<strong>der</strong>en Kontext enstammt.Für das Korrekturlesen möchte ich mich bei meiner Mutter, meiner Freundin Sonja,Claudio Nie<strong>der</strong>, Peter Landmann sowie bei Barbara Steckhan (KollektivDruck-Reif) bedanken. Alle Fehler, die jetzt noch in dieser Arbeit vorhanden sind, gehennatürlich auf meine Person zurück.Abschließend danke ich auch beson<strong>der</strong>s herzlich meinen Eltern für ihr Verständnisund ihre Unterstützung während des Studiums.München, im März 1997Peter FröhlichIn <strong>der</strong> Mathematik ist <strong>der</strong> höchste Grad von innerer Gewißheitgewöhnlich nicht gleich am Anfang zu finden, son<strong>der</strong>nan einem späteren Punkt; daher geben die früherenAbleitungen, bis sie diesen Punkt erreichen, eher Grund,die Voraussetzungen zu glauben, weil wahre Folgerungendaraus hervorgehen, als die Folgerungen zu glauben, weilsie aus den Voraussetzungen hervorgehen.— Whitehead & Russel, [62]


Inhaltsverzeichnis1 Einführung 11.1 Das Babel <strong>der</strong> Programmiersprachen . . . . . . . . . . . . . . . . . . . . 21.2 Die Programmiersprache Oberon-2 . . . . . . . . . . . . . . . . . . . . . 21.3 <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.4 Exkurs: Zum Ideal <strong>der</strong> Einfachheit . . . . . . . . . . . . . . . . . . . . . 41.5 Aufbau dieser Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Die Programmiersprache Oberon-2 72.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82.1.1 Klassifizierung und Terminologie . . . . . . . . . . . . . . . . . . 82.1.2 Aufbau von Oberon-2 Programmen . . . . . . . . . . . . . . . . . 82.2 Typkonzept . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92.2.1 Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92.2.2 Elementare und strukturierte Typen . . . . . . . . . . . . . . . . 102.2.3 Zeiger- und Prozedurtypen . . . . . . . . . . . . . . . . . . . . . . 102.3 Imperative Konzepte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122.3.1 Zuweisungen und Kontrollstrukturen . . . . . . . . . . . . . . . . 122.3.2 Prozeduren und Funktionsprozeduren . . . . . . . . . . . . . . . . 132.4 Objektorientierte Konzepte . . . . . . . . . . . . . . . . . . . . . . . . . 152.4.1 Typerweiterung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152.4.2 Zusicherung und Prüfung von Typen . . . . . . . . . . . . . . . . 162.4.3 Typgebundene Prozeduren . . . . . . . . . . . . . . . . . . . . . . 182.5 Modulkonzept . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182.5.1 Import und Export . . . . . . . . . . . . . . . . . . . . . . . . . . 182.5.2 Module und Klassen . . . . . . . . . . . . . . . . . . . . . . . . . 192.6 Beispiel: Grundgerüst einer graphischen Applikation . . . . . . . . . . . . 202.6.1 <strong>Entwicklung</strong> <strong>der</strong> einzelnen Klassen . . . . . . . . . . . . . . . . . 202.6.2 Integration in das Oberon-System . . . . . . . . . . . . . . . . . . 232.7 Kritische Anmerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252.7.1 Repetitive Anweisungen und Einfachheit . . . . . . . . . . . . . . 252.7.2 Vordeklarierte Datentypen und Portabilität . . . . . . . . . . . . 262.7.3 Prozedurtypen und Objektorientierung . . . . . . . . . . . . . . . 272.7.4 Oberon-2 und das Oberon-System . . . . . . . . . . . . . . . . . . 27


viInhaltsverzeichnis3 Der Oberon-2 Compiler OP2 293.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303.1.1 Einphasen- und Mehrphasen-Compiler . . . . . . . . . . . . . . . 303.1.2 Die Frontend-Backend-Architektur . . . . . . . . . . . . . . . . . 303.1.3 Modularisierung von OP2 . . . . . . . . . . . . . . . . . . . . . . 313.2 Lexikalische und Syntaktische Analyse . . . . . . . . . . . . . . . . . . . 323.2.1 Scanner (OPS.Mod) . . . . . . . . . . . . . . . . . . . . . . . . . 323.2.2 Parser (OPP.Mod) . . . . . . . . . . . . . . . . . . . . . . . . . . 323.3 Interne Repräsentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333.3.1 Datentypen (OPT.Mod) . . . . . . . . . . . . . . . . . . . . . . . 333.3.2 Verwaltung <strong>der</strong> Symboltabelle (OPT.Mod) . . . . . . . . . . . . . 373.3.3 Generierung des Syntaxbaums (OPB.Mod) . . . . . . . . . . . . . 383.3.4 Aufbau <strong>der</strong> Symboldateien . . . . . . . . . . . . . . . . . . . . . . 413.4 Generierung von Objektcode . . . . . . . . . . . . . . . . . . . . . . . . . 443.4.1 Datentypen (OPL.Mod) . . . . . . . . . . . . . . . . . . . . . . . 443.4.2 Traversieren des Syntaxbaums (OPV.Mod) . . . . . . . . . . . . . 453.4.3 High-level Codegenerator (OPC.Mod) . . . . . . . . . . . . . . . . 463.4.4 Low-level Codegenerator (OPL.Mod) . . . . . . . . . . . . . . . . 503.4.5 Aufbau <strong>der</strong> Objektdateien . . . . . . . . . . . . . . . . . . . . . . 543.5 Kritische Anmerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 553.6 Allgemeine Än<strong>der</strong>ungen für <strong>Fro<strong>der</strong>on</strong> . . . . . . . . . . . . . . . . . . . . 564 Spezifikation, Axiomatische Semantik und Korrektheit 574.1 Einführung und Motivation . . . . . . . . . . . . . . . . . . . . . . . . . 584.1.1 Korrektheit als Qualitätsmerkmal . . . . . . . . . . . . . . . . . . 584.1.2 Testen o<strong>der</strong> Beweisen? . . . . . . . . . . . . . . . . . . . . . . . . 584.1.3 Axiomatische Semantik . . . . . . . . . . . . . . . . . . . . . . . . 594.1.4 Vertragliches Programmieren . . . . . . . . . . . . . . . . . . . . 604.2 Grundlagen <strong>der</strong> axiomatischen Semantik . . . . . . . . . . . . . . . . . . 614.2.1 Terminologie und Notation . . . . . . . . . . . . . . . . . . . . . . 614.2.2 Einfache Anweisungen und Anweisungsfolgen . . . . . . . . . . . . 634.2.3 Fallunterscheidungen . . . . . . . . . . . . . . . . . . . . . . . . . 654.2.4 Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 674.2.5 Prozeduren und Funktionsprozeduren . . . . . . . . . . . . . . . . 704.2.6 Abstrakte Datentypen . . . . . . . . . . . . . . . . . . . . . . . . 724.2.7 Probleme für reale Programmiersprachen . . . . . . . . . . . . . . 744.3 Spracherweiterungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 764.3.1 Vor- und Nachbedingungen für Prozeduren . . . . . . . . . . . . . 764.3.2 Varianten und Invarianten für Schleifen . . . . . . . . . . . . . . . 814.3.3 Invarianten für Klassen und Module . . . . . . . . . . . . . . . . . 834.3.4 Aufbau <strong>der</strong> Ausdrücke in Zusicherungen . . . . . . . . . . . . . . 854.4 Implementierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 874.4.1 Allgemeine Än<strong>der</strong>ungen in Scanner und Parser . . . . . . . . . . . 884.4.2 Prüfung <strong>der</strong> Ausdrücke in Zusicherungen . . . . . . . . . . . . . . 88


Inhaltsverzeichnisvii4.4.3 Wo sollten die Prüfung durchgeführt werden? . . . . . . . . . . . 894.4.4 Vor- und Nachbedingungen für Prozeduren . . . . . . . . . . . . . 904.4.5 Varianten und Invarianten für Schleifen . . . . . . . . . . . . . . . 934.4.6 Invarianten für Module . . . . . . . . . . . . . . . . . . . . . . . . 944.4.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . 964.5 Alternativen in Oberon-2 . . . . . . . . . . . . . . . . . . . . . . . . . . . 964.5.1 Die vordeklarierte Prozedur ASSERT() . . . . . . . . . . . . . . . 974.5.2 Vor- und Nachbedingungen für Prozeduren . . . . . . . . . . . . . 974.5.3 Invarianten und Varianten für Schleifen . . . . . . . . . . . . . . . 984.5.4 Invarianten für Klassen und Module . . . . . . . . . . . . . . . . . 994.5.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . 1015 Prozeduren, Funktionen und Parameterübergabe 1035.1 Einführung und Motivation . . . . . . . . . . . . . . . . . . . . . . . . . 1045.1.1 Parameterübergabe: Spezifikation und Implementierung . . . . . 1045.1.2 Seiteneffekte in Funktionsprozeduren . . . . . . . . . . . . . . . . 1065.2 Spracherweiterungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1085.2.1 Konstante Prozedurparameter . . . . . . . . . . . . . . . . . . . . 1085.2.2 Seiteneffektfreie Funktionen . . . . . . . . . . . . . . . . . . . . . 1125.3 Implementierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1135.3.1 Allgemeine Än<strong>der</strong>ungen in Scanner und Parser . . . . . . . . . . . 1135.3.2 Konstante Prozedurparameter . . . . . . . . . . . . . . . . . . . . 1145.3.3 Seiteneffektfreie Funktionen . . . . . . . . . . . . . . . . . . . . . 1156 Klassen, Typen und Objektorientierung 1196.1 Einführung und Motivation . . . . . . . . . . . . . . . . . . . . . . . . . 1206.1.1 Sichtbarkeit in Klassen . . . . . . . . . . . . . . . . . . . . . . . . 1206.1.2 Abstrakte Klassen und Methoden . . . . . . . . . . . . . . . . . . 1226.1.3 Subtyping und Subclassing . . . . . . . . . . . . . . . . . . . . . . 1236.1.4 Einfach- und Mehrfachvererbung . . . . . . . . . . . . . . . . . . 1256.1.5 Kovarianz und Kontravarianz . . . . . . . . . . . . . . . . . . . . 1296.2 Spracherweiterungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1326.2.1 Ein neuer Sichtbarkeitsbereich für Klassen . . . . . . . . . . . . . 1326.2.2 Abstrakte Klassen und Methoden . . . . . . . . . . . . . . . . . . 1356.2.3 Signaturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1406.2.4 Mehrfaches Subtyping . . . . . . . . . . . . . . . . . . . . . . . . 1436.3 Implementierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1466.3.1 Allgemeine Än<strong>der</strong>ungen in Scanner und Parser . . . . . . . . . . . 1466.3.2 Ein neuer Sichtbarkeitsbereich für Klassen . . . . . . . . . . . . . 1466.3.3 Abstrakte Klassen und Methoden . . . . . . . . . . . . . . . . . . 1486.3.4 Signaturen und Mehrfaches Subtyping . . . . . . . . . . . . . . . 151


viiiInhaltsverzeichnis7 Zusammenfassung und Ausblick 1597.1 Ergebnisse dieser Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . 1607.2 Verwandte Ansätze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1647.3 Die weitere <strong>Entwicklung</strong> von Oberon und Oberon-2 . . . . . . . . . . . . 166A Weitere Verbesserungsvorschläge für Oberon-2 169A.1 Unregelmäßigkeiten <strong>der</strong> Syntax . . . . . . . . . . . . . . . . . . . . . . . 170A.2 Unstrukturierte Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . 170A.3 Prozedurtypen- und variablen . . . . . . . . . . . . . . . . . . . . . . . . 170A.4 Vergleich von Prozedurvariablen . . . . . . . . . . . . . . . . . . . . . . . 171A.5 Zeigertypen und <strong>der</strong>en Deklaration . . . . . . . . . . . . . . . . . . . . . 172Literaturverzeichnis 175


Kapitel 1EinführungAt the present time I think we are on the verge of discoveringat last what programming languages should reallybe like. I look forward to seeing many responsible experimentswith language design during the next few years; andmy dream is that by 1984 we will see a consensus developingfor a really good programming language (or, morelikely, a coherent family of languages).— Donald E. Knuth, in [39]In diesem einführenden Kapitel beschäftigen wir uns damit, warum es immer noch keineProgrammiersprache gibt, wie Knuth siesichinobigemZitatwünscht, und warum auchheute noch verantwortungsvolle Experimente“ nötig sind, um ihr näher zu kommen.”Außerdem motivieren wir die Weiterentwicklung von Oberon-2 und stellen das <strong>Projekt</strong><strong>Fro<strong>der</strong>on</strong> vor. Schließlich geben wir noch einen Überblick über den <strong>weiteren</strong> Aufbaudieser Arbeit.


2 Einführung1.1 Das Babel <strong>der</strong> ProgrammiersprachenLei<strong>der</strong> hat Knuth mit seiner Hoffnung von 1974 nicht recht behalten. Auch über 20 Jahrenach seiner ”Vision“ ist kein endgültiger Konsens darüber in Sicht, was eine ”wirklichgute Programmiersprache“ ausmacht. Offensichtlich sind immer noch ”verantwortungsvolleExperimente“ nötig, um die Frage zu beantworten, in welcher Programmiersprachewohl die Computer an Bord <strong>der</strong> USS Enterprise NCC1701-D 1 programmiert wordensind.In [4] zählt Grady Booch etwa 2000 verschiedene Programmiersprachen, die seitden 60er Jahren entstanden sind. Als Gründe für dieses ”Babel <strong>der</strong> Programmiersprachen“führt Booch folgendes an:• Neue Problembereiche machen neue Programmiersprachen nötig o<strong>der</strong> lassen sichmit neuen Programmiersprachen leichter beschreiben.• Mit je<strong>der</strong> neuen Programmiersprache wächst das Verständnis dafür, was für Programmiersprachenan sich wichtig ist und was nicht.• Zwischen <strong>der</strong> Weiterentwicklung von Programmiersprachen und <strong>der</strong> Weiterentwicklung<strong>der</strong> (theoretischen) Informatik besteht eine enge Beziehung.Diese Begründungen zeigen vor allem zwei Dinge: Zum einen, daß alleine aufgrundneuer Anfor<strong>der</strong>ungen und Erkenntnisse neue Programmiersprachen entstehen können,zum an<strong>der</strong>en aber auch, daß die Entstehung neuer Programmiersprachen wie<strong>der</strong> zurWeiterentwicklung <strong>der</strong> Programmiersprachen im allgemeinen beiträgt.Vor allem diese Rückkopplung läßt Zweifel daran aufkommen, ob jemals die idealeFamilie von Programmiersprachen gefunden wird (o<strong>der</strong> gefunden werden sollte), in<strong>der</strong> Knuth gerne programmieren würde. Es erscheint wahrscheinlicher, daß auch inden kommenden Jahrzehnten viele neue Programmiersprachen das Licht <strong>der</strong> Welt erblicken“,ihren Beitrag leisten und dann wie<strong>der</strong> sterben“ werden. 2 ””1.2 Die Programmiersprache Oberon-2Um ”verantwortungsvolle Experimente“ mit Programmiersprachen im Sinne von Knuthdurchzuführen, bieten sich grundsätzlich zwei Möglichkeiten an: Entwe<strong>der</strong> es wird einekomplett neue Programmiersprache für das Experiment entworfen, o<strong>der</strong> eine schonexistierende Programmiersprache wird entsprechend verän<strong>der</strong>t. Beide Vorgehensweisenhaben Vor- und Nachteile; im wesentlichen sind daher praktische Erwägungen (undnicht zuletzt auch persönliche Präferenzen) entscheidend. Für diese Arbeit wurde die1 Nur um Verwechslungen auszuschließen: Es handelt sich hierbei um das Flaggschiff <strong>der</strong> Fö<strong>der</strong>ation<strong>der</strong> Vereinten Planeten aus <strong>der</strong> Science-Fiction-Serie ”Star Trek“ und nicht um irgendein an<strong>der</strong>es Schiffgleichen Namens.2 Philosophisch gesprochen: Wie auch im richtigen Leben ist nicht die <strong>Entwicklung</strong> dieser ”wirklichguten“ Programmiersprachen das eigentliche Ziel; vielmehr ist das Streben nach ihrer <strong>Entwicklung</strong>wichtig.


1.3 <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> 3zweite Möglichkeit gewählt: Es wird ein experimenteller Nachfolger einer bestehendenProgrammiersprache entwickelt.Als Ausgangspunkt wurde die Programmiersprache Oberon-2 [46,47] gewählt, <strong>der</strong>enaktuelle Sprachdefinition auch bei [13] zu finden ist. Sie ist (wie ihr Vorgänger Oberon)im Rahmen von <strong>Projekt</strong> Oberon entstanden, einem (je nach Standpunkt ganz bzw.teilweise abgeschlossenen) <strong>Projekt</strong> <strong>der</strong> ETH Zürich, das 1985 von Niklaus Wirth undJürg Gutknecht initiiert wurde. Mehr zu diesem <strong>Projekt</strong>, aus dem auch ein eigenesBetriebssystem hervorgegangen ist, findet sich in [65] und auch bei [40].Die Gründe für die Wahl <strong>der</strong> Programmiersprache Oberon-2 sind vielfältiger Natur.Zunächst ist ihr Umfang klein und überschaubar, was es erleichtert, unerwünschte Wechselwirkungenzwischen alten und neuen Sprachkonzepten zu erkennen. Dazu kommt, daßCompiler für Oberon-2 auf diversen Plattformen verfügbar sind, meist sogar im Quelltext.Außerdem zeigen viele Arbeiten, die sich direkt o<strong>der</strong> indirekt mit Oberon o<strong>der</strong>Oberon-2 beschäftigen, daß es auch in einer kompakten und ausgereiften Programmierspracheimmer noch Möglichkeiten zur Verbesserung gibt, ohne ihre grundlegenden Ideenaufzugeben. Es seien hier vor allem die an <strong>der</strong> ETH Zürich durchgeführten Dissertationen[5,11,16,26,59,61] sowie die technischen Berichte [9,17,18,27,30,41,54,57] genannt,auf die wir in dieser Arbeit immer wie<strong>der</strong> zu sprechen kommen werden.Schließlich spielen natürlich auch meine persönlichen Interessen eine Rolle, dennals Anwen<strong>der</strong> von Oberon-2 auf unterschiedlichen Plattformen und generell an Programmierspracheninteressierter Student habe ich verschiedene — meiner Meinung nachwichtige — Sprachkonzepte in Oberon-2 vermißt.Im Rahmen dieser Arbeit werden nun Konzepte für Programmiersprachen vorgestellt,diskutiert und in die Programmiersprache Oberon-2 integriert. Dies geschieht allerdingsnicht als sinnfreie Übung, um einen Diplomtitel zu erlangen, son<strong>der</strong>n als Teil einesgrößeren Vorhabens, das ich <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> getauft habe.1.3 <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong>Im Rahmen von <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> soll — wie<strong>der</strong>um durch ”verantwortungsvolle Experimente“im Sinne von Knuth —aneinemmöglichen Nachfolger zu Oberon-2 gearbeitetwerden. Dies geschieht zum einen durch die Identifikation von Problemen, die sich aus<strong>der</strong> Sprachdefinition von Oberon-2 ergeben, zum an<strong>der</strong>en durch Experimente mit (imBezug auf Oberon-2) neuen Sprachkonzepten.Diese Experimente sollen jeweils zu einer ”neuen“ Sprache führen (in dieser Arbeitführen sie zu <strong>der</strong> <strong>Entwicklung</strong> von <strong>Fro<strong>der</strong>on</strong>-1), für die zur Kontrolle des Entwurfsauch ein Compiler in <strong>der</strong> Sprache selbst geschrieben wird. Wir sehen im folgenden ausZeitgründen davon ab, einen neuen Compiler zu implementieren. Prinzipiell soll aberauch <strong>der</strong> hier vorgestellte erste Dialekt von <strong>Fro<strong>der</strong>on</strong> seinen eigenen Compiler bekommen.Die einzelnen Dialekte von <strong>Fro<strong>der</strong>on</strong> müssen keineswegs kompatibel miteinan<strong>der</strong> sein,sollten aber jeweils möglichst nahe an Oberon-2 bleiben, um etwaige Probleme mit <strong>der</strong>Basissprache früh erkennen zu können. Kompatibilitätsfragen zwischen den Dialekten


4 Einführungwerden erst dann wie<strong>der</strong> aufgegriffen, wenn sie mit Oberon-2 zu einem endgültigenNachfolger zusammengeführt werden sollen; bis dahin sind sie als eigenständig zu betrachten.Erklärtes Ziel des <strong>Projekt</strong>s ist es, den Nachfolger für Oberon-2 ”so kompatibelwie möglich“, aber auch ”so verschieden wie nötig“ zu machen. Um die resultierendeSprache klein zu halten, sollen keinesfalls zuviele Kompromisse geschlossen werden.Weitere Informationen zu <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> können auf elektronischem Weg beip.froehlich@link-m.de eingeholt werden.1.4 Exkurs: Zum Ideal <strong>der</strong> EinfachheitEin wichtiger Punkt, <strong>der</strong> durch die Wahl von Oberon-2 als Ausgangssprache erst insBlickfeld rückt, ist die Einfachheit einer Programmiersprache als eines ihrer Qualitätsmerkmale.Das gesamte <strong>Projekt</strong> Oberon wurde von Wirth und Gutknecht nach demEinsteinschen IdealMake it as simple as possible, but not simpler.geplant und durchgeführt. Natürlich sind auch die Programmiersprachen <strong>der</strong> Oberon-Familie auf dieses Ideal <strong>der</strong> Einfachheit hin entwickelt worden. In [63] schreibt Wirthzur <strong>Entwicklung</strong> <strong>der</strong> Programmiersprache Oberon und den Unterschieden zu Modula-2:It soon became clear that the rule to concentrate on the essential and toeliminate the inessential should not only be applied to the design of thenew system, but equally stringently to the language in which the system isformulated.Da sich diese Arbeit mit Erweiterungen <strong>der</strong> Programmiersprache Oberon-2 beschäftigt,die natürlich eine gewisse Erhöhung <strong>der</strong> Komplexität zur Folge haben, sollten wir kritischnachfragen, ob sich diese Erweiterungen mit Blick auf das Ideal <strong>der</strong> Einfachheitüberhaupt rechtfertigen lassen.Daß sowohl Oberon als auch Oberon-2 für sich das Ideal <strong>der</strong> Einfachheit in Anspruchnehmen, hilft uns in dieser Frage weiter. Offensichtlich ist Oberon-2 komplexer als Oberon,weil im Gegensatz zu <strong>der</strong> <strong>Entwicklung</strong> von Oberon aus Modula-2 Erweiterungeneingebracht wurden, ohne überflüssige“ Elemente zu entfernen. Beide Programmiersprachensind also nur in einer Beziehung so einfach wie möglich“: In <strong>der</strong> Beziehung,””die ihre Entwickler definiert haben. Bei <strong>der</strong> <strong>Entwicklung</strong> von Oberon stellte Wirth bestimmteFor<strong>der</strong>ungen auf, denen Oberon gerecht werden sollte, und machte die Sprachedann so einfach wie möglich“. Bei <strong>der</strong> <strong>Entwicklung</strong> von Oberon-2 stellte Mössenböck”bestimmte — von Wirth abweichende — For<strong>der</strong>ungen auf und machte die Sprache dannso einfach wie möglich“.”Das Einsteinsche Ideal handelt also nicht von dogmatisch verordneter Einfachheit,son<strong>der</strong>n vom Streben nach Einfachheit, um ein konkretes, a priori bekanntes Ziel zuerreichen. Der erreichbare Grad an Einfachheit ist relativ zu diesem Ziel. 33 Es verwun<strong>der</strong>t natürlich nicht, daß gerade Einstein eine in dieser Form relative Aussage gemachthat.


1.5 Aufbau dieser Arbeit 5Damit haben wir auch die Antwort auf unsere Frage nach <strong>der</strong> Rechtfertigung vonErweiterungen gefunden. In dieser Arbeit wird ein an<strong>der</strong>es Ziel verfolgt als bei <strong>der</strong><strong>Entwicklung</strong> von Oberon und Oberon-2: eineüber diese beiden Sprachen hinausgehendeUnterstützung von Techniken, die wir zur Erstellung von sicheren und erweiterbarenSoftware-Systemen für wichtig erachten. Es gilt also, diese Erweiterungen als konkreteZiele zu formulieren und die entstehende Sprache dabei trotzdem so einfach wie möglichzu halten. Wenn dies gelingt, darf auch <strong>Fro<strong>der</strong>on</strong>-1 das Ideal <strong>der</strong> Einfachheit für sich inAnspruch nehmen.1.5 Aufbau dieser ArbeitDer Aufbau dieser Arbeit glie<strong>der</strong>t sich wie folgt: In Kapitel 2 werden knapp die wichtigstenGrundlagen <strong>der</strong> Programmiersprache Oberon-2 dargestellt und einige kritischeAnmerkungen zur aktuellen Sprachdefinition gemacht. Kapitel 3 stellt den Oberon-2-Compiler OP2 vor, in den später die verschiedenen Erweiterungen für <strong>Fro<strong>der</strong>on</strong>-1integriert werden sollen.Mit Kapitel 4 beginnt <strong>der</strong> Hauptteil dieser Arbeit. Hier werden die Grundlagen <strong>der</strong>Spezifikation von Programmen durch die Verwendung axiomatischer Semantik dargestelltund die entsprechenden Spracherweiterungen erarbeitet. Kapitel 5 beschäftigt sichmit Erweiterungen am Konzept <strong>der</strong> Prozeduren bzw. Funktionsprozeduren und <strong>der</strong>enRealisierung. In Kapitel 6 werden schließlich die Grundlagen unseres Modells <strong>der</strong> Objektorientierungvorgestellt und ebenfalls entsprechende Spracherweiterungen und <strong>der</strong>enImplementierung diskutiert.Abschließend werden in Kapitel 7 unsere Ergebnisse nochmals zusammengefaßt, undes wird ein Ausblick auf weitere Sprachkonzepte und zukünftige Programmiersprachengegeben.In Anhang A sind weitere Probleme von Oberon-2 sowie Erweiterungsvorschlägezusammengefaßt, die ebenfalls in das <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> einfließen sollen.


Kapitel 2Die Programmiersprache Oberon-2This report is not intended as a programmer’s tutorial. Itis intentionally kept concise. Its function is to serve asa reference for programmers, implementors, and manualwriters. What remains unsaid is mostly left so intentionally,either because it is <strong>der</strong>ivable from the stated rules ofthe language, or because it would require to commit thedefinition when a general commitment appears unwise.— Niklaus Wirth, in [63]In diesem Kapitel stellen wir die für diese Arbeit wichtigen Sprachkonzepte von Oberon-2 vor und demonstrieren sie anhand eines ”größeren“ Beispiels für die objektorientierteProgrammierung. Wer sich jedoch genauer mit <strong>der</strong> Programmierung in Oberon o<strong>der</strong>Oberon-2 beschäftigen will, <strong>der</strong> wird an folgenden Büchern — die sich im übrigen sehrgut ergänzen — nicht vorbeikommen: In [53] wird eine grundlegende Einführung in dieProgrammierung mit Oberon gegeben, Oberon-2 wird dort allerdings nur in einem Anhangbehandelt. Dagegen beschäftigt sich [47] ausschließlich mit <strong>der</strong> objektorientiertenProgrammierung in Oberon-2, geht aber von gewissen Grundkenntnissen einer an<strong>der</strong>enstrukturierten Programmiersprache wie Pascal o<strong>der</strong> Modula-2 aus.


8 Die Programmiersprache Oberon-22.1 Einführung2.1.1 Klassifizierung und TerminologieDie Programmiersprache Oberon-2 ist eine Hybridsprache,dieneben ”klassischen“ prozeduralenund imperativen Elementen auch objektorientierte Programmierung unterstützt.Die klassischen Elemente von Oberon-2 orientieren sich stark an Pascal und Modula-2.Typkonzept, Kontrollstrukturen, Prozeduren, Funktionsprozeduren und die Grundlagendes Modulkonzepts sind fast vollständig aus Modula-2 übernommen worden. Siesollten für einen Programmierer, <strong>der</strong> mit dieser Sprache vertraut ist, keinerlei Problemdarstellen.Die objektorientierten Konzepte von Oberon-2 weichen aber sowohl im Bezug aufdie verwendete Terminologie als auch was Sichtbarkeitsaspekte angeht von an<strong>der</strong>en objektorientiertenProgrammiersprachen — wie zum Beispiel C++ o<strong>der</strong> Eiffel —ab. Zumeinen spricht man in Oberon-2 nicht von Klassen und Methoden, son<strong>der</strong>n von erweiterbarenRecordtypen und typgebundenen Prozeduren. Zum an<strong>der</strong>en wird die Sichtbarkeitvon Fel<strong>der</strong>n dieses Recordtyps und an ihn gebundenen Prozeduren nicht durch die Klassebestimmt, son<strong>der</strong>n durch das umgebende Modul.Dies trägt zwar <strong>der</strong> sinnvollen Unterscheidung zwischen den orthogonalen Konzeptenvon erweiterbaren Recordtypen, typgebundenen Prozeduren sowie Modulen Rechnung,führt aber bei Diskussionen über verschiedene objektorientierte Programmiersprachenzu Verwechslungen. Wir werden deshalb im folgenden auch für Oberon-2 die AusdrückeMethode (für eine typgebundene Prozedur), Klasse (für einen erweiterbare Recordtypmit Methoden) sowieObjekt (für eine zur Laufzeit existierende Instanz einer Klasse)benutzen.2.1.2 Aufbau von Oberon-2 ProgrammenEin Programm besteht in Oberon-2 aus einer Reihe von Modulen, die sowohl Deklarationenvon Konstanten, Typen, Variablen und Prozeduren enthalten als auch eineAnweisungsfolge, die zur Initialisierung eines Moduls ausgeführt wird. Je<strong>der</strong> deklarierteBezeichner kann exportiert und damit für an<strong>der</strong>e Module sichtbar gemacht werden.In Oberon-2-Programmen, die unter dem Oberon-System erstellt wurden und lauffähigsind, gibt es kein zentrales Hauptmodul wie etwa bei Modula-2-Programmen. Andessen Stelle treten sogenannte Kommandos. Unter einem Kommando versteht dasOberon-System eine exportierte, parameterlose und nicht typgebundene Prozedur. Einesolche Prozedur kann in <strong>der</strong> Form Modul.Prozedur direkt von <strong>der</strong> Benutzeroberflächeaus gestartet werden. Das entsprechende Modul (und alle <strong>weiteren</strong> von diesem Modulimportierten Module) wird bei Bedarf geladen, dynamisch gebunden und initialisiert,bevor die Kontrolle an das aufgerufene Kommando abgegeben wird. Dort werden dannmeist Parameter ausgewertet und die internen Prozeduren zur Realisierung des Kommandosaufgerufen.Compiler für Oberon-2, die unter konventionellen Betriebssystemen lauffähige Programmeerzeugen, müssen allerdings meist statisch binden. Sie postulieren deswegen


2.2 Typkonzept 9die Existenz eines Hauptmoduls, das auch als Eintrittspunkt für den Aufruf des Programmsdient. Auf die Probleme dieser Trennung zwischen Oberon-System und an<strong>der</strong>enBetriebssystemen gehen wir in Abschnitt 2.7.4 nochmals näher ein.2.2 Typkonzept2.2.1 GrundlagenIn Oberon-2 wird jedem Bezeichner (explizit für Variablen, implizit für Konstanten undProzeduren) ein Typ zugeordnet, <strong>der</strong> zur Vermeidung von Fehlern bei verschiedenenPrüfungen herangezogen wird. 1 Die genauen Definitionen <strong>der</strong> verschiedenen Relationenzwischen zwei Typen (zum Beispiel, wann sie denselben Typ darstellen, wann siegleich sind, wann zwei Variablen dieser Typen zuweisungskompatibel sind) werden in<strong>der</strong> Sprachdefinition [46] in einem Anhang gegeben.Wichtig für das Folgende ist vor allem, daß Oberon-2 — bis auf eine Ausnahme —Namens-Äquivalenz zwischen Typen verwendet: Zwei Typen, die zwar die gleiche Strukturhaben, aber separat deklariert wurden, sind nicht äquivalent. Zum Beispiel sind dieTypen A = ARRAY 16 OF INTEGER und B = ARRAY 16 OF INTEGER nicht äquivalent,die Typen C = ARRAY 16 OF INTEGER und D = C schon. Anonyme Typen, also solche,für die kein eigener Bezeichner vereinbart wurde, sind damit nie zu an<strong>der</strong>en Typenäquivalent.Die angesprochene Ausnahme wird für Zuweisungen von Prozeduren an Prozedurvariablengemacht, da für Prozeduren aufgrund <strong>der</strong> Syntax von Oberon-2 keine expliziteDeklaration ihres Typs möglich ist. 2 Anstelle von Namens-Äquivalenz wird hier diestrukturelle Äquivalenz <strong>der</strong> Signaturen verwendet. In Anhang A weisen wir auf Problemedieser Ausnahmeregelung hin. Für Konstanten treten im übrigen trotz <strong>der</strong> fehlendenexpliziten Deklaration ihres Typs keine Probleme auf, da sie lediglich vordeklarierte, elementareTypen haben können.Für den Begriff des Typs“ ist es außerdem wichtig zu unterscheiden, in welchem”Zusammenhang er verwendet wird. So sprechen wir zum Beispiel in objektorientiertenSystemen vom Typ“ eines Objekts (bzw. einer Klasse) und meinen damit die Schnittstelledes Objekts (bzw. <strong>der</strong> Klasse). In einer Programmiersprache wie Oberon-2 wird”aber jedem Bezeichner ein Typ“ zugeordnet, so daß hier zwei separat deklarierte Klassen”mit identischen Schnittstellen unterschiedliche Typen“ haben. Im folgenden müssen”wir uns deshalb immer im klaren darüber sein, ob wir von einem Typ im Sinne <strong>der</strong> Objektoriertierungo<strong>der</strong> von einem Typ im Sinne einer Programmiersprache wie Oberon-2reden.1 Der Typ einer Typdeklaration ist <strong>der</strong> deklarierte Typ selbst; es gibt keine Metatypen. Diese Aussagefinden wir zwar nicht in <strong>der</strong> Sprachdefinition [46], sie wird aber aus den Quelltexten des Compilers OP2(siehe Kapitel 3) evident.2 Eine Prozedur verstehen wir hier im Sinne einer ”Prozedurkonstanten“; Prozedurtypen und Prozedurvariablenhaben natürlich einen explizit deklarierten Typ.


10 Die Programmiersprache Oberon-22.2.2 Elementare und strukturierte TypenAn vordeklarierten elementaren Typen bietet Oberon-2 BOOLEAN, CHAR und SET, dieganzzahligen Typen SHORTINT, INTEGER und LONGINT sowie die rationalen Typen REALund LONGREAL. Zwischen den numerischen Typen besteht die RelationSHORTINT ⊆ INTEGER ⊆ LONGINT ⊆ REAL ⊆ LONGREALdie auch Typ-Einschluß genannt wird: Die Werte eines Typs liegen im Wertebereich desihn einschließenden Typs. In Ausdrücken können numerische Typen deswegen gemischtauftreten. 3 Der Typ SET dient zur Modellierung von Mengen ganzer Zahlen kleinerMächtigkeit (zur Implementierung von SET wird meist ein Maschinenwort verwendet).Die folgende Tabelle enthält einige Beispiele zu den elementaren Typen.Deklaration ZugriffVAR b: BOOLEAN b := FALSEVAR c: CHAR c := "a"VAR i: INTEGER i := 10VAR x: REAL x := 3.14; x := i;VAR s: SET s := {0, i, 17}Es können auch zwei Arten von komplexeren, strukturierten Typen definiert werden:Arraytypen stellen Reihungen identischer Elementtypen dar, auf die durch einenganzzahligen Index zugegriffen werden kann; Recordtypen stellen Kombinationen verschiedenerElementtypen dar, auf die durch einen eindeutigen Bezeichner zugegriffenwerden kann. Die folgende Tabelle enthält einige Beispiele zu den strukturierten Typen.DeklarationZugriffVAR a: ARRAY 16 OF INTEGER a[12] := 10VAR r: RECORD x,y: INTEGER END r.x := 10; r.y := 202.2.3 Zeiger- und ProzedurtypenStrukturierte Typen können als Basistypen für Zeigertypen verwendet werden. Die Variableeines Zeigertyps liefert als Wert eine an<strong>der</strong>e, anonyme Variable des Typs, auf densie verweist. Durch Zeigertypen können rekursive und dynamische Datenstrukturen erzeugtwerden. Als Basistyp eines Zeigertyps kann auch ein Arraytyp ohne Angabe einerGröße verwendet werden; damit kann die Größe eines Arrays zur Laufzeit festgelegtwerden. Die vordeklarierte Prozedur NEW() dient zur Erzeugung von anonymen Variablen.4 Das folgende Beispiel illustriert die Verwendung von Zeigertypen zum Aufbaueiner linearen Liste.3 Trotz Typ-Einschluß kann es in Ausdrücken notwendig sein, die Typen <strong>der</strong> beteiligten Variablenund Konstanten explizit zu än<strong>der</strong>n. Hierzu bietet Oberon-2 die vordeklarierten Prozeduren ENTIER(),SHORT() und LONG() an.4 Da Oberon-2 überlicherweise automatische Speicherbereinigung (garbage collection) unterstützt,wird keine vordeklarierte Prozedur zur Freigabe von Speicher, den anonyme Variablen belegen, angeboten.


2.2 Typkonzept 11TYPE Data = RECORD (* ... *) END;Node = POINTER TO NodeDesc;NodeDesc = RECORDnext: Node;data: Data;END;VAR a: Data; n: Node;Wird über einen Zeiger auf Komponenten des Basistyps zugegriffen, so wird <strong>der</strong> Zeigerautomatisch <strong>der</strong>eferenziert. Die Zuweisungen a := n.data und a := n^.data sind alsoidentisch. Lediglich dann, wenn <strong>der</strong> Basistyp selbst benötigt wird, ist eine expliziteDereferenzierung nötig.Auch Prozeduren haben in Oberon-2 einen Typ und es können Prozedurtypen, Prozedurvariableno<strong>der</strong> formale Parameter vom Typ einer Prozedur deklariert werden. Ineiner numerischen Anwendung könnte zum Beispiel ein Integrationsverfahren für skalareFunktionen mit <strong>der</strong> SchnittstellePROCEDURE Area (lowerBound, upperBound: REAL;function: ScalarFunction): REAL;vereinbart sein, zusammen mit <strong>der</strong> Typ-VereinbarungTYPE ScalarFunction = PROCEDURE (x: REAL): REAL;die die Signaturen aller mit Area numerisch integrierbaren Funktionen beschreibt. Diedurch die FunktionsprozedurPROCEDURE Linear (x: REAL): REAL;BEGIN RETURN 2*x+1 END;beschriebene Funktion y =2x + 1 kann dann zum Beispiel durch den Aufrufarea := Area (-1, 3, Linear)numerisch im Bereich [−1, 3] ⊂ R integriert werden. In Oberon-2 wird allerdings dieEinschränkung gemacht, daß nur globale Prozeduren in dieser Weise verwendet werdenkönnen. Dem Compiler bleibt es so erspart, zu allen Prozeduren ihren jeweiligen Kontext(lexical closure) mitzuführen (siehe auch [64, S. 121ff und S156f]). 55 In [3] wird allerdings gezeigt, daß lexical closures durchaus ihre Berechtigung haben können, zumBeispiel als Ersatz für die in [21] beschriebenen Entwurfsmuster Strategy und Iterator. Viele funktionaleSprachen verfügen über dieses Konzept, und auch in SmallTalk ist es in Form von blocks vorhanden.


12 Die Programmiersprache Oberon-22.3 Imperative Konzepte2.3.1 Zuweisungen und KontrollstrukturenDer charakteristische Zug imperativer Programmierung ist das Vorhandensein einer Zuweisungin <strong>der</strong> Programmiersprache. Diese ermöglicht beliebige Än<strong>der</strong>ungen des Zustands— also <strong>der</strong> Werte aller Variablen — eines Programms. Daneben haben sich aberauch typische Formen von Kontrollstrukturen entwickelt, über die an<strong>der</strong>e Programmiersprachen(zum Beispiel funktionale Sprachen wie Pure-LISP)in<strong>der</strong>Regelnichtverfügen.Zuweisungen werden in Oberon-2 durch den Operator := beschrieben. Auf <strong>der</strong> linkenSeite des Operators muß ein Ausdruck stehen, dessen Wert eine Variable ist. Auf <strong>der</strong>rechten Seite kann ein beliebiger, mit dieser Variable zuweisungskompatibler Ausdruckstehen. Zwischen den numerischen Datentypen wird die Zuweisungskompatibilität zumBeispiel durch die Typeinschluß-Relation definiert.An Kontrollstrukturen bietet Oberon-2 zunächst die Fallunterscheidungen IF undCASE, die zum Beispiel in einem Scanner für eine Programmiersprache wie folgt verwendetwerden könnten:IF ("a"


2.3 Imperative Konzepte 13c := 0;FOR i := 0 TO 2 DOINC (c, a[i] * b[i]);ENDc := 0; i := 0;WHILE i


14 Die Programmiersprache Oberon-2Von <strong>der</strong> Art ihrer Verwendung her unterscheiden sich Prozeduren und Funktionsprozedurendarin, für welche syntaktischen Elemente <strong>der</strong> Sprache ihr Aufruf stehen kann:Prozeduraufrufe können anstelle einer Anweisung stehen und stellen eine Abstraktionfür Anweisungen dar; Funktionsprozeduren können anstelle eines Ausdrucks stehen undstellen eine Abstraktion für Ausdrücke dar. Funktionsprozeduren sollten deswegen freivon Seiteneffekten sein, da auch Ausdrücke frei von Seiteneffekten sind. Mit diesemProblem beschäftigen wir uns aber auch in Kapitel 5 nochmals genauer.In Oberon-2 existieren zwei verschiedene Mechanismen zur Übergabe von Parameternan Prozeduren und Funktionsprozeduren:• formale Wertparameter (value substitution, call by value), <strong>der</strong>en zugehörige aktuelleParameter Ausdrücke sein können, die vor Aufruf <strong>der</strong> Prozedur ausgewertetund <strong>der</strong>en Werte lokal in <strong>der</strong> Prozedur gespeichert werden, und• formale Referenzparameter (variable substitution, call by reference), <strong>der</strong>en zugehörigeaktuelle Parameter Variablen (als Spezialfall von Ausdrücken) sein müssenund die innerhalb <strong>der</strong> Prozedur als lokaler Alias für diese Variable gelten.Die Übergabe als Referenzparameter bzw. Wertparameter wird bei <strong>der</strong> Deklaration <strong>der</strong>formalen Parameter einer Prozedur syntaktisch durch die Präsenz bzw. Absenz desSchlüsselworts VAR angezeigt. Da formale Referenzparameter lediglich einen lokalen Aliasdarstellen, also einen lokalen Namen für die übergebene Variable, kann innerhalb <strong>der</strong>Prozedur durch Zuweisungen an den Alias <strong>der</strong> Wert des aktuellen Parameters außerhalb<strong>der</strong> Prozedur verän<strong>der</strong>t werden.Innerhalb des Deklarationsteils von Prozeduren können weitere Prozeduren deklariertwerden. Prozeduren lassen sich also beliebig schachteln. Die Sichtbarkeit lokaler undglobaler Bezeichner wird durch folgende Regeln definiert: 71. Der Sichtbarkeitsbereich eines Bezeichners erstreckt sich textuell von seiner Deklarationbis zum Ende des Blocks, zu dem die Deklaration gehört und zu dem erlokal ist.2. Der Sichtbarkeitsbereich eines Bezeichners schließt an<strong>der</strong>e Sichtbarkeitsbereichevon Bezeichnern gleichen Namens, die in einem außen liegenenden Block deklariertwurden, aus.3. Der Name eines Bezeichners muß in seinem Block eindeutig sein; <strong>der</strong> Name darfnur innerhalb seines Sichtbarkeitsbereiches benutzt werden.Ein Block ist in diesem Zusammenhang ein Modul bzw. eine Prozedur. Diese Definition<strong>der</strong> Sichtbarkeit wird auch als Block-Struktur bezeichnet.7 Die Regeln für Zeigertypen und Recordfel<strong>der</strong> wurden hier weggelassen.


2.4 Objektorientierte Konzepte 152.4 Objektorientierte Konzepte2.4.1 TyperweiterungRecordtypen können in Oberon-2 so um neue Datenfel<strong>der</strong> erweitert werden, daß <strong>der</strong>neue erweiterte Recordtyp und <strong>der</strong> alte Recordtyp zuweisungskompatibel bleiben. Sokönnten zum Beispiel in einer Applikation, die Daten von Studenten und Dozenten aneiner Hochschule verwalten soll, folgende Recordtypen deklariert werden:TYPEPersonDesc = RECORDname: String;birthdate: Date;(* ... *)END;StudentDesc = RECORD (PersonDesc)semester: INTEGER;(* ... *)END;LecturerDesc = RECORD (PersonDesc)salary: INTEGER;(* ... *)END;VARstudent: StudentDesc; person: PersonDesc;In diesem Beispiel werden Studenten und Dozenten als Erweiterungen von Personenmodelliert, da beide Daten beinhalten, die für jede Person, ganz gleich ob Student o<strong>der</strong>Dozent, relevant sind. Erweiterte Recordtypen sind zu ihren Basistypen zuweisungskompatibel,allerdings werden zum Beispiel bei <strong>der</strong> Zuweisung person := student lediglichdie Fel<strong>der</strong> von StudentDesc, die auch in PersonDesc vorhanden sind, zugewiesen. Wirsprechen hier von einer <strong>Projekt</strong>ion des erweiterten Recordtyps auf seinen Basistyp. Eineumgekehrte Zuweisung <strong>der</strong> Form student := person ist natürlich nicht zulässig, da siemit <strong>der</strong> Semantik <strong>der</strong> Zuweisung nicht zu vereinbaren ist: Mit welchem Wert sollte zumBeispiel dann das Feld Student.semester belegt werden?Diese Situation än<strong>der</strong>t sich, wenn wir neben den Recordtypen entsprechende Zeigertypeneinführen:TYPEPerson = POINTER TO PersonDesc;Student = POINTER TO StudentDesc;Lecturer = POINTER TO LecturerDesc;(* other types as above *)VARstudent: Student; person: Person;


16 Die Programmiersprache Oberon-2Anmerkung 2.1 Ein Oberon-2 Idiom für nicht erweiterbare KlassenBei <strong>der</strong> <strong>Entwicklung</strong> eines objektorientierten Systems tritt manchmal <strong>der</strong> Wunsch nachnicht weiter erweiterbaren Klassen auf. Da in Oberon-2 eine abgeleitete Klasse nurdurch die Angabe eines bereits deklarierten Recordtyps eingeführt werden kann, bietetsich für nicht erweiterbare Klassen folgendes Schema an:TYPEKlasse = POINTER TO RECORD (* ... *) END;Da <strong>der</strong> Recordtyp jetzt nicht mehr explizit auftritt, können keine Erweiterungen mehrdeklariert werden. Methoden für solche Klassen können allerdings nur auf den Zeigertypenvereinbart werden.Für Zeigertypen (und auch für Recordtypen, die als Referenzparameter übergeben werden)müssen wir zwischen dem statischen Typ einer Zeigervariable und ihrem dynamischenTyp unterscheiden. Die Variable person hat zum Beispiel den statischen TypPerson. Aber nach einer Zuweisung <strong>der</strong> Form person := student, wobeistudent aufein Record vom Typ StudentDesc verweisen soll, än<strong>der</strong>t sich ihr dynamischer Typ aufStudent. Diese Eigenschaft ist für die Erzeugung von heterogenen dynamischen Datenstrukturenunbedingt erfor<strong>der</strong>lich.Die für Recordvariablen nicht erlaubte Zuweisung student := person ist dann (beikorrekten dynamischen Typen) möglich, wenn auch nur in begrenzter Weise (siehe Abschnitt2.4.2).2.4.2 Zusicherung und Prüfung von TypenUm Aussagen über den dynamischen Typ eines Bezeichners machen zu können, bietetOberon-2 Typ-Zusicherungen (type guards) und Typ-Prüfungen (type tests) an.Zusicherungen werden nach einem Bezeichner in Klammern angegeben und stehenfür die Aussage: Dieser Bezeichner hat hier den angegebenen Typ o<strong>der</strong> einen davonabgeleiteten. Wenn zum Beispiel die Variable person den dynamischen Typ Student hat,ist die Zuweisung student := person(Student) zulässig. <strong>Zur</strong> Laufzeit wird an dieserStelle zunächst <strong>der</strong> zugesicherte dynamische Typ überprüft. Wenn die Zusicherung nichtwahr ist, wird ein Laufzeitfehler erzeugt. Ansonsten wird <strong>der</strong> Bezeichner im aktuellenAusdruck so behandelt, als wäre er vom zugesicherten Typ.Typ-Zusicherungen haben kosmetische Ähnlichkeiten zu Typ-Umwandlungen (typecasts) in Sprachen wie C und C++. Im Gegensatz dazu werden Typ-Zusicherungenaber durch das Laufzeitsystem überprüft; sie stellen also ein sicheres Konzept und keinRisiko dar. 88 Durch diese Prüfungen wird die Effizienz eines Programms sicherlich beeinträchtigt, aber bei weitemnicht so stark, wie wir annehmen könnten. Jede Prüfung kann in konstanter Zeit (üblicherweise2 Zyklen des Prozessors) durchgeführt werden. Darüber hinaus findet sich in [9] ein relativ einfachesOptimierungs-Verfahren, mit dessen Hilfe <strong>der</strong> Compiler die Erzeugung gewisser Prüfungen für dynamischeTypen vermeiden kann.


2.4 Objektorientierte Konzepte 17Außer den eher deklarativen Typ-Zusicherungen sind in Oberon-2 aber auch explizitePrüfungen des dynamischen Typs durch den Operator IS möglich. Damit können wirFallunterscheidungen in Abhängikeit vom dynamischen Typ eines Bezeichners realisieren.Eine Prozedur, die sowohl Personen als auch Studenten und Dozenten verarbeitensoll, können wir zum Beispiel wie folgt deklarieren:PROCEDURE Process (VAR person: PersonDesc);BEGINIF person IS StudentDesc THEN(* Zugriff auf Fel<strong>der</strong> von StudentDesc mittels Typeguard möglich *)ELSIF person IS LecturerDesc THEN(* Zugriff auf Fel<strong>der</strong> von LecturerDesc mittels Typeguard möglich *)ELSE(* Verarbeitung von Personen *)END;END Process;Allerdings sollte diese Möglichkeit nicht zu oft genutzt werden, da Methoden (sieheAbschnitt 2.4.3) eine bessere lokale Bindung an den jeweiligen Typ und damit eineflexiblere Struktur des Gesamtsystems erlauben.Schließlich existiert in Oberon-2 auch noch eine Mischung aus Typ-Zusicherung undTyp-Prüfung: die WITH-Anweisung. Diese hat syntaktisch die Struktur einer CASE-Anweisung, wobei die einzelnen Klauseln allerdings verschiedene dynamische Typendarstellen. Damit können wir die obige Prozedur einfacher in folgen<strong>der</strong> Form deklarieren:PROCEDURE Process (VAR person: PersonDesc);BEGINWITH person: StudentDesc DO(* Zugriff auf Fel<strong>der</strong> von StudentDesc ohne Typeguard möglich *)| person: LecturerDesc DO(* Zugriff auf Fel<strong>der</strong> von LecturerDesc ohne Typeguard möglich *)ELSE(* Verarbeitung von Personen *)END;END Process;Hinter dem Variablenbezeichner aus dem Kopf einer Klausel wird innerhalb <strong>der</strong> Klauselimmer eine entsprechende Typ-Zusicherung impliziert. Aus diesem Grund wird die WITH-Anweisung auch als regionale Typ-Zusicherung (regional type guard) bezeichnet.Die Verwendung <strong>der</strong> WITH-Anweisung ist allerdings umstritten, da sie es in gewissenFällen ermöglicht, das strenge Typ-System von Oberon-2 zu überlisten. Auf dieseProbleme gehen wir in Anhang A nochmals genauer ein.


18 Die Programmiersprache Oberon-22.4.3 Typgebundene ProzedurenMethoden werden in Oberon-2 durch typgebundene Prozeduren beschrieben. Diese werdenbei ihrer Deklaration durch einen zusätzlichen Empfängerparameter (receiver parameter)in<strong>der</strong>FormPROCEDURE (self: Person) Print ();o<strong>der</strong>PROCEDURE (VAR self: PersonDesc) Print ();gekennzeichet. Beide Deklarationen führen eine Methode Print() ein, um zum Beispieldie Daten von Personen in schriftlicher Form auf den Bildschirm zu bringen. In unseremBeispiel könnte ihr Aufruf etwa durch person.Print() erfolgen, vorausgesetzt personwurde deklariert und mit sinnvollen Werten initialisiert. Die Deklarationen unterscheidensich lediglich darin, für welche Art von Empfänger (Recordtyp o<strong>der</strong> Zeigertyp) sieverwendet werden können:• Prozeduren die an Zeigertypen gebunden werden, können auch nur durch Variablendes Zeigertyps aufgerufen werden.• Prozeduren die an Recordtypen gebunden werden, können sowohl durch Variablendes Recordtyps als auch durch Variablen des Zeigertyps aufgerufen werden; einZeigertyp wird auch hier automatisch <strong>der</strong>eferenziert.Typgebundene Prozeduren können für erweiterte Typen überschrieben bzw. redefiniertwerden. Innerhalb einer redefinierten Methode kann aber immer noch auf dieüberschriebene Methode des Basistyps zugegriffen werden; dies geschieht durch einenAufruf <strong>der</strong> Form self.Methode^(), wennself <strong>der</strong> jeweilige Empfängerparameter ist.Die Methode Print() muß beispielsweise für Studenten o<strong>der</strong> Dozenten mehr Informationenausgeben, als für Personen. Allerdings sollen die gemeinsamen Informationenfür Personen nicht doppelt ausgegeben werden. Deswegen rufen wir in <strong>der</strong> MethodePrint() <strong>der</strong> Klasse Student sinnvollerweise zunächst die entsprechende Methode <strong>der</strong>Basisklasse Person auf. Dort werden die allen Personen gemeinsamen Daten ausgegeben,bevor wir dann die spezifischen Daten eines Studenten ausgeben.2.5 Modulkonzept2.5.1 Import und ExportDie durch einen Oberon-2-Compiler übersetzbaren Einheiten sind Module. Wie in Abschnitt2.1.2 beschrieben, werden innerhalb eines Moduls verschiedene Deklarationen zueiner Einheit zusammengefaßt. Bezeichner, die für an<strong>der</strong>e Module sichtbar sein sollen,werden durch die Markierung *“ exportiert. Sie können aber auch durch die Markierung”-“ exportiert werden und sind dann in externen Modulen schreibgeschützt (read-only).”


2.5 Modulkonzept 19Anmerkung 2.2 Getrennte und unabhängige ÜbersetzungWenn die Module eines Programms separat bzw. arbeitsteilig entwickelt werden sollen, istes von beson<strong>der</strong>er Wichtigkeit, daß <strong>der</strong> Compiler ihre konsistente Verwendung sicherstellenkann. In Programmiersprachen wie FORTRAN o<strong>der</strong> C ist dies nur eingeschränkt möglich,indem <strong>der</strong> Programmierer entsprechend diszipliniert vorgeht und zum Beispiel prototypesverwendet. Die Module“ werden hier unabhängig voneinan<strong>der</strong> übersetzt, und lediglich <strong>der</strong>”Bin<strong>der</strong> (linker) kann einige <strong>der</strong> möglichen Inkonsistenzen zwischen den Modulen“ feststellen.”Darunter fallen aber meistens nur die Namen <strong>der</strong> Bezeichner und nicht <strong>der</strong>en Typen. Da abergerade das Typ-Konzept entscheidend zur Sicherheit von Hochsprachen beiträgt, kann manes nicht plötzlich an <strong>der</strong> Modulgrenze fallenlassen“. Oberon-2 beschreitet hier einen an<strong>der</strong>en”Weg: Bei <strong>der</strong> Übersetzung eines Moduls wird nicht nur eine Objektdatei, son<strong>der</strong>n auch eineSymboldatei erzeugt, die in kompakter Form alle Informationen enthält, um Typ-Prüfungenüber Modulgrenzen hinweg durchzuführen. Im Gegensatz zur obengenannten unabhängigenÜbersetzung sprechen wir hier von getrennter Übersetzung.Module dienen hauptsächlich <strong>der</strong> Strukturierung eines Programms in überschaubareund wie<strong>der</strong>verwendbare Einheiten. In diesem Punkt sind sie Klassen nicht unähnlich,allerdings existiert von einem Modul immer nur eine ”Instanz“ zur Laufzeit des Programms.Für die <strong>Entwicklung</strong> von Software ist es außerdem wichtig, daß verschiedeneModule von verschiedenen Entwicklern erstellt werden können. Um ein konsistentes Systemsicherzustellen, sind dazu natürlich Prüfungen zwischen den Modulen nötig (sieheauch Anmerkung 2.2).2.5.2 Module und KlassenDas Modulkonzept von Oberon-2 hat Auswirkungen auf die objektorientierten Konzepte<strong>der</strong> Sprache.Da Module und nicht Klassen für Sichtbarkeitaspekte zuständig sind, können innerhalbeines Moduls mehrere, eng kooperierende Klassen deklariert werden, die ihreInterna gegenseitig kennen und verwenden. Um beispielsweise einen allgemeinen Containerauf <strong>der</strong> Basis linearer Listen zu realisieren, können wir das Klassenpaar List undLink in einem Modul Lists deklarieren. So ist auch ohne ”Umwege“ in <strong>der</strong> Programmiersprache— wie den friend-Mechanismus in C++ o<strong>der</strong> limitierte Sichtbarkeit inEiffel — eine effiziente und sichere Realisierung möglich.Wir können aber in Oberon-2 auch das übliche Klassenkonzept, wie es in C++ o<strong>der</strong>Eiffel vorhanden ist, durch Module simulieren, indem wir lediglich eine Klasse pro Moduldeklarieren.Ein Problem stellen Module in Oberon-2 allerdings für die Erweiterbarkeit von objektorientiertenSystemen dar, denn es wird lediglich zwischen öffentlicher und privaterSichtbarkeit unterschieden. Abgeleitete Klassen können also nicht auf die Interna ihrerBasisklasse zugreifen, außer sie wurden öffentlich (und damit für alle Module sichtbar)exportiert. In Kapitel 6 beschäftigen wir uns näher mit diesem Problem und bieten aucheine Lösung dafür an.


20 Die Programmiersprache Oberon-22.6 Beispiel: Grundgerüst einer graphischen ApplikationUm Oberon-2 auch ”praktisch“ vorzustellen, wollen wir — inspiriert durch [4] — in diesemAbschnitt das Grundgerüst einer einfachen objektorientierten Applikation erstellen.Es handelt sich um eine graphische Applikation, die mit geometrischen Formen umgehtund diese verwalten muß. Konkret könnte dies ein graphischer Editor o<strong>der</strong> ein Simulationspaketsein, aber darauf kommt es im folgenden nicht an. Wir beschäftigen unsnur mit dem Problem, die geometrischen Formen Kreis, Rechteck und (als Variation)gefülltes Rechteck auf den Bildschirm zu bringen.2.6.1 <strong>Entwicklung</strong> <strong>der</strong> einzelnen KlassenBasisklasse ShapeZunächst treffen wir die Entscheidung, die geometrischen Formen stets von ihrem Mittelpunktaus zu betrachten, <strong>der</strong> in allen Fällen eindeutig bestimmt ist. Natürlich benötigenwir noch weitere Informationen für konkrete Formen, aber allen gemeinsam ist ein Mittelpunkt.Da unsere Formen natürlich auch gezeichnet werden sollen, müssen wir eine passendeOperation dazu zur Verfügung stellen. Die Definition <strong>der</strong> Basisklasse Shape ist damitleicht, wie in Listing 2.1 gezeigt, anzugeben.Listing 2.1 Implementierung von Shapes.ModMODULE Shapes;IMPORT Display;TYPE Point* = RECORDx*, y*: INTEGER;END;Shape* = POINTER TO ShapeDesc;ShapeDesc* = RECORDcenter: Point;END;PROCEDURE (self: Shape) SetCenter* (p: Point);BEGINself.center := p;END SetCenter;PROCEDURE (self: Shape) Draw* (f: Display.Frame);BEGINHALT (99); (* abstract method *)END Draw;PROCEDURE (self: Shape) GetCenter* (VAR p: Point);BEGINp := self.center;END GetCenter;END Shapes.


2.6 Beispiel: Grundgerüst einer graphischen Applikation 21Wir haben zur Vereinfachung noch einen zusätzlichen Recordtyp Point eingeführt,<strong>der</strong> die Koordinaten eines Punktes in einem kartesischen Koordinatensystem repräsentiert.Die Methode Draw() kann in <strong>der</strong> Basisklasse noch nichts Sinnvolles tun, da hiernoch nicht klar ist, welche konkrete Form gezeichnet werden soll. Durch den Aufruf<strong>der</strong> vordeklarierten Prozedur HALT() simulieren wir eine abstrakte Methode: Wird ineiner abgeleiteten Klasse vergessen, Draw() zu implementieren, wird ein entsprechen<strong>der</strong>Laufzeitfehler erzeugt.Der in <strong>der</strong> Signatur von Draw() auftretende Typ Display.Frame dient im Oberon-System als Grundlage für alle <strong>weiteren</strong> Bildschirmoperationen.Die Klasse CircleAusgehend von <strong>der</strong> Basisklasse Shape lassen sich Kreise zum Beispiel durch ihren Radiusbeschreiben. Wir müssen also mittels Typerweiterung eine abgeleitete Klasse Circleeinführen, die den Radius eines Kreises aufnehmen kann und zusätzliche Methoden fürden Zugriff auf diesen Radius anbietet. Außerdem müssen wir die abstrakte MethodeDraw() für Kreise konkret implementieren. Listing 2.2 zeigt das enstehende Modul fürdie Klasse Circle.Listing 2.2 Implementierung von Circles.ModMODULE Circles;IMPORT Shapes, Display, Display1;TYPE Circle* = POINTER TO CircleDesc;CircleDesc* = RECORD (Shapes.ShapeDesc)radius: INTEGER;END;PROCEDURE (self: Circle) SetRadius* (r: INTEGER);BEGINself.radius := r;END SetRadius;PROCEDURE (self: Circle) Draw* (f: Display.Frame);VAR center: Shapes.Point;BEGINself.GetCenter (center);Display1.Circle (f, Display.white, center.x, center.y, self.radius,Display.invert);END Draw;PROCEDURE (self: Circle) GetRadius* (VAR r: INTEGER);BEGINr := self.radius;END GetRadius;END Circles.Die entsprechende Operation, um im Oberon-System einen Kreis zu zeichnen, ist indem Modul Display1 versteckt. Ihre Benutzung sollte aus dem Listing offensichtlichwerden.


22 Die Programmiersprache Oberon-2Die Klasse RectangleUm Rechtecke behandeln zu können, müssen wir — ähnlich wie oben für Kreise —die zusätzlich notwendigen Daten (Breite und Höhe) durch Erweiterung <strong>der</strong> BasisklasseShape einfließen lassen. Daneben sind natürlich entsprechende Zugriffs-Methodenfür diese Daten vorzusehen, und auch die Methode Draw() muß wie<strong>der</strong> implementiertwerden. Die Klasse Rectangle wird daher wie in Listing 2.3 gezeigt aufgebaut.Listing 2.3 Implementierung von Rects.ModMODULE Rects;IMPORT Shapes, Display, Display1;TYPE Rectangle* = POINTER TO RectangleDesc;RectangleDesc* = RECORD (Shapes.ShapeDesc)width, height: INTEGER;END;PROCEDURE (self: Rectangle) SetWidth* (w: INTEGER);BEGINself.width := w;END SetWidth;PROCEDURE (self: Rectangle) SetHeight* (h: INTEGER);BEGINself.height := h;END SetHeight;PROCEDURE (self: Rectangle) Draw* (f: Display.Frame);VAR center: Shapes.Point; x0, y0, x1, y1: INTEGER;BEGINself.GetCenter (center);x0 := center.x - self.width DIV 2; y0 := center.y - self.height DIV 2;x1 := x0 + self.width; y1 := y0 + self.height;Display1.Line (f, Display.black, x0, y0, x1, y0, Display.invert);Display1.Line (f, Display.black, x1, y0, x1, y1, Display.invert);Display1.Line (f, Display.black, x1, y1, x0, y1, Display.invert);Display1.Line (f, Display.black, x0, y1, x0, y0, Display.invert);END Draw;PROCEDURE (self: Rectangle) GetWidth* (VAR w: INTEGER);BEGINw := self.width;END GetWidth;PROCEDURE (self: Rectangle) GetHeight* (VAR h: INTEGER);BEGINh := self.height;END GetHeight;END Rects.Lei<strong>der</strong> bietet das Oberon-System keine elementare Operation zum Zeichnen vonRechtecken an, so daß wir gezwungen sind, entsprechend einzelne Linien zu ziehen.


2.6 Beispiel: Grundgerüst einer graphischen Applikation 23Die Klasse SolidRectangleGefüllte Rechtecke unterscheiden sich von normalen Rechtecken lediglich durch die Tatsache,daß ihr Inneres mit einer Farbe ausgefüllt wird. Da wir stets die gleiche Farbezum Füllen verwenden, in <strong>der</strong> wir auch das Rechteck selbst zeichnen, müssen wir lediglichdie Methode Draw() abän<strong>der</strong>n; es brauchen keine neuen Datenfel<strong>der</strong> deklariert zuwerden.Listing 2.4 Implementierung von SolidRects.ModMODULE SolidRects;IMPORT Shapes, Rects, Display;TYPE SolidRectangle* = POINTER TO SolidRectangleDesc;SolidRectangleDesc* = RECORD (Rects.RectangleDesc)END;PROCEDURE (self: SolidRectangle) Draw* (f: Display.Frame);VAR center: Shapes.Point; VAR x, y, w, h: INTEGER;BEGINself.Draw^ (f);self.GetCenter (center); self.GetWidth (w); self.GetHeight (h);x := center.x - w DIV 2; y := center.y - h DIV 2;x := x + 1; y := y + 1; w := w - 1; h := h - 1;Display.ReplConstC (f, Display.black, x, y, w, h, Display.invert);END Draw;END SolidRects.Die Än<strong>der</strong>ung erfolgt — wie in Listing 2.4 gezeigt — allein dadurch, daß wir nachdem Aufruf <strong>der</strong> Methode <strong>der</strong> Basisklasse Rectangle durch self.Draw^ (f) eine entsprechendeOperation des Oberon-Systems aufrufen, um die Fläche auszufüllen.2.6.2 Integration in das Oberon-SystemUm unser kleines Beispiel in das Oberon-System zu integrieren, müssen wir ein Modulerstellen, das eine Zeichenfläche eröffnet, einzelne graphische Objekte erzeugt und dieseschließlich zeichnet. In Listing 2.5 ist die Implementierung dieses Moduls gezeigt.Wollen wir unsere Klassen testen, so müssen wir nacheinan<strong>der</strong> die einzelnen KommandosShapesDemo.Open, ShapesDemo.GenerateShapes und ShapesDemo.Draw über dieBenutzerschnittstelle aufrufen. Die in Listing 2.5 verwendeten Teile des Oberon-Systemwollen wir hier nicht im einzelnen erläutern. Allerdings weisen wir darauf hin, daß es sichhier nicht um ein gutes Beispiel für dessen Programmierung handelt; dazu müßten aberviele an<strong>der</strong>e Bereiche einbezogen werden, die das Beispiel lediglich komplexer machen,ohne unserem Ziel, die Praxis von Oberon-2 zu veranschaulichen, dienlich zu sein.


24 Die Programmiersprache Oberon-2Anmerkung 2.3 <strong>Zur</strong> Namensgebung von EmpfängerparameternIn Oberon-2 kann <strong>der</strong> Name des Empfängerobjekts durch seine explizite Deklaration freigewählt werden. Diese Tatsache wird zum Beispiel in [47, S. 43f] als Vorteil gewertet, daein aussagekräftiger und problembezogener Name gewählt werden kann, während wir uns zumBeispiel in C++ mit this o<strong>der</strong> in SmallTalk mit self zufriedengeben müssen. Wenn manallerdings eine abgeleitete Klasse (zugegebenermaßen aus Bequemlichkeit) ausgehend von einertextuellen Kopie <strong>der</strong> Basisklasse definiert, so kommt einem eine konsequente Namensgebung,die stets den gleichen Namen für das Empfängerobjekt wählt, sehr entgegen. Außerdem ist einedurchgehende Namensgebung leichter lesbar, da man nicht immer die Signatur <strong>der</strong> Methodezu Rate ziehen muß, um zu erkennen, welche Variable den Empfänger bezeichnet. Aus diesenGründen verwenden wir in unserem Beispiel immer den Namen self für den Empfänger.Listing 2.5 Implementierung von ShapesDemo.ModMODULE ShapesDemo;IMPORT Shapes, Circles, Rects, SolidRects, Oberon, MenuViewers,TextFrames, Viewers, Display, Input;VAR p: Shapes.Point; c: Circles.Circle; r: Rects.Rectangle;s: SolidRects.SolidRectangle;shapes: ARRAY 3 OF Shapes.Shape;v: MenuViewers.Viewer;PROCEDURE Draw* ();VAR i: INTEGER;BEGIN (* draw our shapes using polymorphism *)FOR i := 0 TO 2 DO shapes[i].Draw (v); END;END Draw;PROCEDURE* Handler(f: Display.Frame; VAR m: Display.FrameMsg);BEGIN (* dummy handler *)END Handler;PROCEDURE Open* ();VAR x, y: INTEGER; f: Display.Frame;BEGINNEW(f); f.handle := Handler;Oberon.AllocateUserViewer(Oberon.Mouse.X, x, y);v := MenuViewers.New(TextFrames.NewMenu("ShapesDemo", "System.Close"),f, TextFrames.menuH, x, y);Display.ReplConstC (f, Display.black, f.X, f.Y, f.W, f.H, Display.replace);END Open;PROCEDURE GenerateShapes* ();BEGINNEW (c); p.x := 100; p.y := 50; c.SetCenter (p); c.SetRadius (20);shapes[0] := c;NEW (r); p.x := 150; p.y := 50; r.SetCenter (p); r.SetWidth (20); r.SetHeight (20);shapes[1] := r;NEW (s); p.x := 200; p.y := 50; s.SetCenter (p); s.SetWidth (20); s.SetHeight (20);shapes[2] := s;END GenerateShapes;END ShapesDemo.


2.7 Kritische Anmerkungen 252.7 Kritische AnmerkungenDieser Abschnitt setzt sich kritisch mit einigen Elementen <strong>der</strong> ProgrammierspracheOberon-2 auseinan<strong>der</strong> und zeigt, in welcher Form sie sich problematisch auswirkenkönnen. Im Rahmen dieser Arbeit hätten natürlich einige dieser Probleme behobenwerden können, allerdings hätten wir dafür die volle Abwärtskompatibilität zwischen<strong>Fro<strong>der</strong>on</strong>-1 und Oberon-2 aufgeben müssen. Wir begnügen uns hier deswegen damit,auf diese Probleme hinzuweisen. Weitere Probleme bzw. Verbesserungsvorschläge, dievon an<strong>der</strong>en Autoren identifiziert bzw. publiziert wurden, sind in Anhang A zusammengefaßt.Auch sie sollen natürlichindas<strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> einfließen.2.7.1 Repetitive Anweisungen und EinfachheitBei <strong>der</strong> ansonsten im Rahmen von <strong>Projekt</strong> Oberon vertretenen Einstellung zur Einfachheitverwun<strong>der</strong>n zunächst die vier Arten von Schleifen (FOR, WHILE, REPEAT, LOOP),die eigentlich nicht nötig wären. Natürlich hat Wirth diesen Punkt auch erkannt undbewußt in Kauf genommen. In [63, Seite 4] schreibt er dazu:Several features of Oberon are superfluous from a purely theoretical pointof view. They are nevertheless retained for practical reasons ...Examplesof such features are the presence of several forms of repetitive statements...They complicate neither the language conceptually nor the compiler toany significant degree.Allerdings steht dieser letzte Satz auf ”wackligen Beinen“, denn ob die Sprache dadurchwirklich nicht komplizierter wird, kann nicht mit Sicherheit objektiv beurteilt werden.Außerdem stellt zum Beispiel Griesemer in [26] folgendes fest:Within a compiler, the implementation effort for LOOP statements is higherthan for other control structures (except the CASE statement), due to thenecessity to handle corresponding EXIT statements at any nesting level.Damit ist zumindest klar, daß die LOOP-Anweisung komplizierter zu realisieren ist alsalle an<strong>der</strong>en Schleifen.Es stellt sich die Frage, ob es nicht angebracht gewesen wäre, die klassischen Formenvon Schleifen in <strong>der</strong> Algol-Tradition ganz zu verlassen und eine einzige repetitiveAnweisung wie in Eiffel zu verwenden. Dort hat jede Schleife die Formfromuntilloopend


26 Die Programmiersprache Oberon-2wobei hier die Teile für Varianten und Invarianten (siehe Kapitel 4) unterschlagen wurden.Zu den Schleifen ist außerdem anzumerken, daß Knut Hildebrand und Jan-Armin Reepmeyer in einer empirischen Untersuchung [34] an <strong>der</strong> Universität Münsterfestgestellt haben, daß REPEAT-Schleifen von Anfängern seltener korrekt verwendet werdenals WHILE-Schleifen. Auch das scheint unter an<strong>der</strong>em ein Grund zu sein, sich ehermit einer einzigen Schleife zu behelfen, wenn auch nicht unbedingt in <strong>der</strong> Form vonEiffel.2.7.2 Vordeklarierte Datentypen und PortabilitätElementare numerische Datentypen werden von Oberon-2 in verschiedenen Bereichsgrößen(und Genauigkeiten bei rationalen Zahlen) angeboten. Die Unterteilung <strong>der</strong>ganzzahligen Datentypen in drei verschiedene Bereiche wurde offensichtlich von <strong>der</strong> erstenPlattform, auf <strong>der</strong> Oberon implementiert wurde, inspiriert: Die Prozessoren <strong>der</strong>NS32000-Familie verfügen über die Datentypen Byte, Word und Long mit einer Größevon 8, 16 und 32 Bit.Die Bindung <strong>der</strong> vordeklarierten Bezeichner an die jeweiligen Größen ist zwar nichtexplizit in <strong>der</strong> Sprachdefinition festgehalten, wird aber implizit in allen bekannten Implementierungenvon Oberon und Oberon-2 gemacht. Sogar an <strong>der</strong> ETH Zürich wird soprogrammiert, als ob ein SET immer 32 Bit lang wäre. 9 Diese Bindung schafft Problemefür die Portabilität und Zukunftssicherheit von Oberon-2. DerÜbergang auf eine mo<strong>der</strong>ne64-Bit-Plattform, wie zum Beispiel den Motorola-PowerPC-604-Prozessor, macht eserfor<strong>der</strong>lich, einen <strong>weiteren</strong> vordeklarierten Datentyp einzuführen, um die nunmehr vierverschiedenen Wortgrößen zu berücksichtigen. Alternativ muß man die vorhandenendrei Datentypen an<strong>der</strong>s als bisher auf die vorhandenen Wortgrößen aufteilen; allerdingsentsteht hier immer ein Loch“, denn eine Wortgröße kann dann nicht von Oberon-2 aus”angesprochen werden. Technologische <strong>Entwicklung</strong>en haben so einen direkten und nichterwünschten Einfluß auf die Programmiersprache, die eigentlich eine abstrakte Notationfür Software bieten will.Viel sinnvoller scheint es in diesem Zusammenhang zu sein, lediglich einen ganzzahligenund einen rationalen Datentyp in die Sprache aufzunehmen. Weitere Datentypen,die genauer auf die jeweilige Plattform abgestimmt sind, könnten über das Pseudo-ModulSYSTEM angeboten werden. 10 Dies könnte beispielsweise wie in Tabelle 2.1 gezeigt erreichtwerden.Neben <strong>der</strong> damit gewonnenen Unabhängigkeit von <strong>weiteren</strong> technologischen <strong>Entwicklung</strong>envereinfachen sich nach [26, S. 35f] darüber hinaus auch viele Programme, diemomentan die verschiedenen Typen ohne wirklichen Grund benutzen, da Konversionsoperatorenin gemischten Ausdrücken teilweise wegfallen.9 Hier sei zum Beispiel das Modul Sets.Mod aus dem Compiler-Compiler COCO erwähnt.10 Das Modul SYSTEM ist nicht im Quelltext vorhanden, son<strong>der</strong>n in den Compiler integriert. Einige<strong>der</strong> dort vorhandene Typen und Prozeduren lassen sich in Oberon-2 auch nicht beschreiben, deshalbdie Bezeichnung ”Pseudo-Modul“.


2.7 Kritische Anmerkungen 27Oberon-2INTEGERREALModul SYSTEMINTEGER32INTEGER8INTEGER16INTEGER64REAL32REAL64Tabelle 2.1: Bessere Aufteilung vordeklarierter Datentypen2.7.3 Prozedurtypen und ObjektorientierungWie in Abschnitt 2.2.3 gezeigt, können wir eine Prozedur mit an<strong>der</strong>en Prozeduren parametrisieren.Dazu vereinbaren wir in ihrer Schnittstelle einen formalen Parameter despassenden Prozedurtyps und übergeben ihr beim Aufruf die zu verwendende Prozedurals aktuellen Parameter.Dieses Konzept erbt Oberon-2 von seinen Vorgängersprachen, inklusive Oberon, inwelchen es die einzige Möglichkeit einer solchen Parametrisierung war. Oberon-2 verfügtjedoch über ein weiteres Konzept, mit dem sich eine <strong>der</strong>artige Parametrisierung erreichenläßt: Klassen.Wir könnten beispielsweise für die Prozedur Area() aus Abschnitt 2.2.3 durch dieTypenTYPEScalarFunction = POINTER TO ScalarFunctionDesc;ScalarFunctionDesc = RECORD END;und die MethodePROCEDURE (VAR self: ScalarFunctionDesc) Evaluate (x: REAL): REAL;eine Klasse für skalare Funktionen einführen, mit <strong>der</strong> sich die Verwendung eines Prozedurtypsvermeiden läßt. Die Prozedur Area() hätte damit sogar immer noch dieselbeSignatur wie bei <strong>der</strong> Verwendung von Prozedurtypen.Auch Variablen eines Prozedurtyps lassen sich auf diese Weise durch Objekte ersetzen.Offensichtlich haben Klassen die Verwendung von Prozedurtypen überflüssiggemacht. Im Sinne des Ideals <strong>der</strong> Einfachheit“ sollten Prozedurtypen deswegen aus”Oberon-2 entfernt werden.2.7.4 Oberon-2 und das Oberon-SystemEin weiteres Problem <strong>der</strong> Sprachdefinition von Oberon-2 ist die enge Verflechtung mitdem Oberon-System, demaus<strong>Projekt</strong> Oberon hervorgegangenen Betriebssystem. ZweiBereiche machen diese Verflechtung beson<strong>der</strong>s deutlich: die sogenannten Kommandos(siehe Abschnitt 2.1.2) und die Definition <strong>der</strong> vordeklarierten Prozedur NEW().


28 Die Programmiersprache Oberon-2Kommandos stellen zunächst ein reines Portabilitätsproblem dar: Soll Oberon-2 untereinem Betriebssystem eingesetzt werden, das keine Kommandos unterstützt, muß einseparates Hauptmodul geschrieben werden, das die Kommandos entsprechend in die Benutzeroberflächedieses Betriebssystems integriert. Allerdings sollten solche Details einerImplementierung nicht in <strong>der</strong> Definition einer Programmiersprache, die eine abstrakteNotation für Software bieten will, enthalten sein. Die Sprachdefinition von Oberon-2 [46]tut aber genau das, wenn auch nur in einem Anhang.Die Definition <strong>der</strong> Semantik von NEW() orientiert sich ebenfalls zu stark am Oberon-System. Wie in Abschnitt 2.2.3 kurz angesprochen, verwendet Oberon-2 üblicherweiseeine Speicherverwaltung mit garbage collection. Die Sprache unterstützt deswegen nurdie vordeklarierte Prozedur NEW(), mit <strong>der</strong>en Hilfe eine anonyme Variable für einenZeiger erstellt wird. Laut [46] impliziert nun ein Aufruf <strong>der</strong> Form NEW (somePointer)entwe<strong>der</strong> somePointer # NIL — das heißt es konnte Speicher angefor<strong>der</strong>t werden —o<strong>der</strong> einen Laufzeitfehler.Unter dem Oberon-System ist dieses Verhalten relativ unproblematisch, da alle geradebenutzten Module im Speicher bleiben und <strong>der</strong> aktuelle Zustand jedes Modulsebenfalls erhalten wird. Soll Oberon-2 jedoch im Rahmen eines an<strong>der</strong>en Betriebssystemsverwendet werden, das wie üblich auf ausführbaren Programmen basiert, so wirddem Programmierer durch diese Definition jede Möglichkeit genommen, das Programmkontrolliert zu verlassen. Dies ist um so schlimmer, als Oberon-2 (im Gegensatz zurursprünglichen Version von Oberon) keineMöglichkeit anbietet, in irgendeiner Form aufLaufzeitfehler zu reagieren.Eine einfache Lösung des Problems bietet die in [63] für Oberon verwendete Definitionvon NEW(): konnte kein Speicher mehr belegt werden, wird <strong>der</strong> Zeiger auf den WertNIL gesetzt. Damit würden wir allerdings den Programmierer mit einer Vielzahl vonPrüfungen <strong>der</strong> ArtIF somePointer = NIL THEN (* Speicher erschöpft *) ENDbelasten.Alternativ dazu könnten wir entwe<strong>der</strong> in <strong>der</strong> Sprache selbst o<strong>der</strong> in den Bibliothekeneinen Mechanismus vorsehen, <strong>der</strong> es dem Programmierer erlaubt, Laufzeitfehler zubehandeln. Dies muß nicht zwangsläufig auf ein komplexes, allgemeines System zurBehandlung von Ausnahmen (exception handling) wie in C++ hinauslaufen.In <strong>der</strong> ursprünglichen Version von Oberon hatte zum Beispiel jedes Modul zusätzlichzum BEGIN-Teil auch einen CLOSE-Teil, <strong>der</strong> vor dem Entfernen des Moduls ausgeführtwurde. Damit lassen sich Laufzeitfehler in gewissem Rahmen mit sinnvollem Aufwandbehandeln.Einige Oberon-2-Compiler bieten Erweiterungen in dieser Richtung an, allerdings istohne eine verbindliche Definition bezüglich <strong>der</strong> Portabilität wie<strong>der</strong> nichts gewonnen.


Kapitel 3Der Oberon-2 Compiler OP2A compiler for a new language L serves both as a proof thatan implementation of the language is feasible and as a toolfor practical applications of L and hence the explorationof its usefullness.— Robert Griesemer, in [26]In diesem Kapitel wird <strong>der</strong> Oberon-2-Compiler OP2 vorgestellt, <strong>der</strong> in den <strong>weiteren</strong> Kapitelnals Ausgangspunkt für unseren <strong>Fro<strong>der</strong>on</strong>-1-Compiler dient. OP2 wurde zwischen1989 und 1991 von Régis Crelier und an<strong>der</strong>en an <strong>der</strong> ETH Zürich entwickelt. Mit OP2sollte ein portabler Oberon (ab Mitte 1991 auch Oberon-2) Compiler geschaffen werden,<strong>der</strong> mit möglichst wenig Aufwand an unterschiedliche Prozessor-Architekturen angepaßtwerden kann und dabei trotzdem effizient arbeitet. Die folgende Beschreibung des Compilersist auf die Bedürfnisse dieser Arbeit abgestimmt und dementsprechend knapp.Sie basiert auf den Quelltexten des Compilers <strong>der</strong> AMIGA-Version des Oberon-Systems(zum Beispiel bei ftp://ftp.inf.ethz.ch/pub/Oberon/Amiga zu finden) sowie auf denArbeiten [6, 10, 11].


30 Der Oberon-2 Compiler OP23.1 Einführung3.1.1 Einphasen- und Mehrphasen-CompilerEin Compiler übersetzt Programme von einer Quellsprache (zum Beispiel Oberon-2) ineine Zielsprache (zum Beispiel Maschinensprache für den Motorola-MC68020-Prozessor)und führt während dieses Vorgangs eine Reihe von Prüfungen durch, um Programmierfehlerabzufangen. Die einzelnen Aufgaben, die während dieses Vorgangs zu bewältigensind, teilt man (klassischerweise) in lexikalische Analyse, syntaktische Analyse, Typ-Prüfung und Codegenerierung ein.Eine für die Architektur des Compilers grundlegende Entscheidung ist die Anzahl<strong>der</strong> Phasen, indie<strong>der</strong>Übersetzungsvorgang unterteilt wird. Ein Mehrphasen-Compilerkönnte zum Beispiel jede <strong>der</strong> vier Teilaufgaben in einer getrennten Phase behandelnund die einzelnen Phasen über temporäre Dateien miteinan<strong>der</strong> kommunizieren lassen(die Ausgabe <strong>der</strong> Phase i ist dabei die Eingabe <strong>der</strong> Phase i +1).In einem Einphasen-Compiler laufen die einzelnen Aufgaben verzahnt ab. So beginntbeispielsweise die Codegenerierung bereits nach dem Erkennen einer syntaktischenEinheit wie etwa einer Zuweisung, und nicht erst nachdem das gesamte Programm diean<strong>der</strong>en Aufgaben durchlaufen hat. Der ursprüngliche Oberon-Compiler von Wirth istnach diesem Prinzip aufgebaut [65].Einphasen-Compiler sind durch die enge Verzahnung <strong>der</strong> einzelnen Aufgaben effizienterals Mehrphasen-Compiler. Allerdings ergeben sich aufgrund dieser Verzahnung auchProbleme für die Portabilität: Aufgaben, die unabhängig von einer konkreten Plattformgelöst werden können (etwa die syntaktische Analyse), und Aufgaben, die inhärent von<strong>der</strong> konkreten Plattform abhängig sind (etwa die Codegenerierung), werden zu starkvermischt.3.1.2 Die Frontend-Backend-ArchitekturUm den Compiler OP2 so portabel wie möglich zu halten, ohne zuviel Effizienz zu opfern,wurde ein Kompromiß zwischen Einphasen- und Mehrphasen-Compilern gewählt: OP2wurde als Zweiphasen-Compiler realisiert.Der Compiler ist in ein portables — nicht von <strong>der</strong> konkreten Plattform abhängiges —Frontend und ein spezifisches — auf die konkrete Plattform zugeschnittenes — Backendunterteilt. Das Frontend liest in <strong>der</strong> ersten Phase den Quelltext eines Moduls und erzeugtdaraus zwei komplexe Datenstrukturen: die Symboltabelle und den Syntaxbaum.Diese repräsentieren (vereinfacht gesagt) die Vereinbarungen und Anweisungen aus demQuelltext in geeigneter Form. In <strong>der</strong> zweiten Phase verwendet das Backend diese Datenstrukturen,um ausführbaren Code für die jeweilige Plattform zu erzeugen.Eine — insbeson<strong>der</strong>e durch den Rummel“ um JAVA — interessante Variante dieses”Verfahrens beschreibt Michael Franz in seiner Dissertation [16]. Anstatt wie JA-VA eine virtuelle Maschine zu benutzen, um Portabilität zu erreichen, werden die vomFrontend erzeugten Datenstrukturen in geeigneter Form gespeichert und on-the-fly“”während des Ladevorgangs in für den jeweiligen Rechner ausführbaren Code übersetzt.


3.1 Einführung 31Inzwischen wurde dieses Verfahren <strong>weiteren</strong>twickelt und trägt nun den Namen Juice;weitere Informationen dazu finden sich bei [14].3.1.3 Modularisierung von OP2Der Compiler OP2 ist selbst in Oberon geschrieben und glie<strong>der</strong>t sich in die folgendenModule auf:OPM.Mod: Deklariert Konstanten zur Beschreibung <strong>der</strong> Ziel-Architektur (zum BeispielMaximalwerte für elementare Typen). Implementiert Lese- und Schreiboperationenfür Symbol- und Objektdateien, um von konkreten Betriebssystemen und <strong>der</strong>enKonventionen zu abstrahieren.OPS.Mod: Implementiert den Scanner, <strong>der</strong> den linearen Eingabetext auf Terminalsymbolevon Oberon-2 abbildet. Insbeson<strong>der</strong>e werden Kommentare gefiltert, Zahlenin die interne Darstellung umgewandelt und Schlüsselwörter erkannt.OPP.Mod: Implementiert den Parser, <strong>der</strong> nach dem Prinzip des rekursiven Abstiegs miteinem Symbol look-ahead und ohne Backtracking arbeitet. Der Parser kontrolliertdas gesamte Frontend; er for<strong>der</strong>t Terminalsymbole vom Scanner an und erzeugtsowohl Symboltabelle als auch Syntaxbaum.OPT.Mod: Deklariert Typen und Konstanten zur Verwaltung <strong>der</strong> Symboltabelle und desSyntaxbaums. Implementiert die Operationen auf <strong>der</strong> Symboltabelle sowie dieEin- und Ausgabe von Symboldateien.OPB.Mod: Implementiert die zum Aufbau des Syntaxbaums notwendigen Operationen.OPV.Mod: Implementiert das Traversieren des Syntaxbaums zur Codegenerierung unddas Dekorieren <strong>der</strong> Symboltabelle mit für die Plattform spezifischen Größen. DerTraversierer kontrolliert das gesamte Backend.OPC.Mod: Implementiert die ”höhere“ Ebene <strong>der</strong> Codegenerierung, die auf Mustern fürdie Konstrukte von Oberon-2 basiert.OPL.Mod: Implementiert die ”nie<strong>der</strong>e“ Ebene <strong>der</strong> Codegenerierung, in <strong>der</strong> die eigentlichenMaschinenbefehle erzeugt werden.Compiler.Mod: Die Benutzerschnittstelle des Compilers; in Versionen für das Oberon-System wird hier das Kommando Compile exportiert, das nach den üblichen Konventionenbenutzt werden kann. <strong>Zur</strong> Übersetzung eines Moduls wird zunächst <strong>der</strong>Parser mit dem entsprechenden Quelltext als Parameter aufgerufen. Falls dieserohne Fehlermeldung zurückkehrt, wird <strong>der</strong> erzeugte Syntaxbaum an den Traversiererübergeben, <strong>der</strong> den entsprechenden Maschinencode erzeugt.


32 Der Oberon-2 Compiler OP2Die Erkennung von Fehlern ist auf die verschiedenen Module verteilt; zum Beispiel werdensyntaktische aber auch einige (statische) semantische Fehler in OPP.Mod erkannt,während die Typ-Prüfung größtenteils in OPB.Mod stattfindet. Von einigen Ausnahmenabgesehen ist damit die Struktur von OP2 beschrieben. Die folgenden Abschnitte gehengenauer auf Einzelheiten <strong>der</strong> Implementierung ein.3.2 Lexikalische und Syntaktische Analyse3.2.1 Scanner (OPS.Mod)Der Scanner von OP2 wird durch das Modul OPS.Mod implementiert. Er greift fürEin- und Ausgabeoperationen auf Prozeduren aus dem Modul OPM.Mod zurück. DieSchnittstelle des Moduls ist, wie in Listing 3.1 gezeigt, aufgebaut.Listing 3.1 Schnittstelle von OPS.ModDEFINITION OPS;CONST MaxStrLen = 256;TYPE Name = ARRAY MaxIdLen OF CHAR;String = ARRAY MaxStrLen OF CHAR;VAR name: Name;str: String;numtyp: INTEGER;intval: LONGINT;realval: REAL;lrlval: LONGREAL;PROCEDURE Get(VAR sym: SHORTINT);PROCEDURE Init;END OPS.Die Prozedur Init() versetzt den Scanner in den Anfangszustand zurück, währenddie Prozedur Get() das jeweils aktuelle Symbol in numerisch codierter Form zurückgibt.Die einzelnen Symbole werden in jedem Modul, das ihre Definition benötigt, lokal alsKonstanten deklariert (siehe auch unsere Anmerkungen in Abschnitt 3.5).Die Variablen name, str usw. werden benutzt, um Werte von erkannten lexikalischenKonstrukten für den Parser zur Verfügung zu stellen. Ihre jeweilige Gültigkeit ist implizitan das aktuelle Symbol gekoppelt.Interessant ist, daß zur Erkennung von Schlüsselwörter keine komplexe Datenstruktur1 verwendet wird: Ein geschachteltes CASE-IF-Konstrukt ist offensichtlich effizientgenug.3.2.2 Parser (OPP.Mod)Der Parser von OP2 wird durch das Modul OPP.Mod implementiert. Er benutzt denScanner OPS.Mod, um Terminalsymbole von Oberon-2 zu erkennen, und verwendet die1 Von Wirth wird zum Beispiel oft eine Hashtable für diesen Zweck propagiert, siehe [64, 65].


3.3 Interne Repräsentation 33Module OPT.Mod bzw. OPB.Mod, um die Symboltabelle bzw. den Syntaxbaum aufzubauen.Die Schnittstelle des Moduls ist, wie in Listing 3.2 gezeigt, aufgebaut.Listing 3.2 Schnittstelle von OPP.ModDEFINITION OPP;IMPORT OPT, OPS;PROCEDURE Module(VAR prog: OPT.Node; VAR modName: OPS.Name);END OPP.Die Prozedur Module() versucht, das Modul mit dem in modName übergebenen Namenzu übersetzen. Sollte dies erfolgreich sein, wird in prog <strong>der</strong> dafür erzeugte Syntaxbaumzurückgegeben, ansonsten <strong>der</strong> Wert NIL.Der Aufbau des Parsers (also seine prozedurale Zerlegung) orientiert sich weitgehendan <strong>der</strong> Syntax von Oberon-2 und entspricht damit den von Wirth, zum Beispiel in [64],vertretenen Grundsätzen.3.3 Interne RepräsentationDie interne Repräsentation eines Oberon-2 Moduls teilt sich in zwei komplexe Datenstrukturen,die Symboltabelle und den Syntaxbaum, auf. In <strong>der</strong> Symboltabelle werdenVereinbarungen von Konstanten, Typen, Variablen und Prozeduren verwaltet, währendim Syntaxbaum Anweisungen (zum Beispiel Zuweisungen und Schleifen) sowie Ausdrücke(zum Beispiel a[i] + b[i]) gespeichert werden.Zwischen diesen Datenstrukturen besteht eine Reihe von Beziehungen. Knoten desSyntaxbaums verweisen etwa auf Einträge in <strong>der</strong> Symboltabelle, zum Beispiel wenn ineinem Ausdruck Variablen o<strong>der</strong> deklarierte Konstanten auftreten. Zusammen ergebendiese Datenstrukturen eine genaue, leicht weiterzuverarbeitende Repräsentation des zuübersetzenden Moduls.3.3.1 Datentypen (OPT.Mod)Bevor wir uns den Prozeduren <strong>der</strong> Module OPT.Mod und OPB.Mod zuwenden, die Operationenfür die Verwaltung <strong>der</strong> Symboltabelle und die Generierung des Syntaxbaumsbereitstellen, beschäftigen wir uns zunächst mit den zur Realisierung <strong>der</strong> Datenstrukturennotwendigen Datentypen, die zentral in OPT.Mod deklariert sind.DeklarationenDeklarierte Objekte werden in OP2 durch die in Listing 3.3 gezeigten Typen dargestellt.Sie werden in einem binären Suchbaum verwaltet, zu dessen Organisation die Fel<strong>der</strong>left und right dienen; Suchschlüssel ist <strong>der</strong> Bezeichner des Objekts, <strong>der</strong> in name gespeichertist. Das Feld scope verweist auf das Objekt, in dessen Sichtbarkeitsbereich dasaktuelle Objekt vereinbart ist; durch link wird innerhalb eines Sichtbarkeitsbereichs dieReihenfolge <strong>der</strong> Deklarationen festgehalten.


34 Der Oberon-2 Compiler OP2Listing 3.3 OPT.Mod: Typen zur Beschreibung deklarierter ObjekteObject = POINTER TO ObjDesc;ObjDesc = RECORDleft, right, link, scope: Object;name: OPS.Name;leaf: BOOLEAN;mode, mnolev: SHORTINT;vis: SHORTINT;typ: Struct;conval: Const;adr, linkadr: LONGINT;END;Das Feld leaf gibt für Prozeduren an, ob innerhalb von ihnen weitere Prozedurenaufgerufen werden; für Variablen gibt es an, ob ihre Adressen gebraucht werden. Beidesist für Optimierungen im Backend wichtig, insbeson<strong>der</strong>e für die Entscheidung, Variablendirekt in Registern zu allozieren.In mode wird festgehalten, um was für eine Art von Objekt (zum Beispiel eine Konstanteo<strong>der</strong> eine Prozedur) es sich konkret handelt. Falls mnolev negativ ist, wurde dasObjekt aus dem Modul -mnolev importiert; an<strong>der</strong>nfalls gibt das Feld die Schachtelungstiefean, auf <strong>der</strong> das Objekt vereinbart wurde (für geschachtelte Prozeduren).Das Feld vis zeigt den Exportstatus des Objekts im Bezug auf das Modul an, indem es deklariert wurde; Objekte können intern, extern o<strong>der</strong> extern und schreibgeschützt(read-only) sein.In typ wird ein Verweis auf den Typ des deklarierten Objekts verwaltet. Das Feldconval enthält numerische Attribute (siehe unten). Die beiden Fel<strong>der</strong> adr und linkadrschließlich werden innerhalb des Backends zur Speicherbelegung benutzt.TypenAuch Typen müssen in <strong>der</strong> Symboltabelle geeignet repräsentiert werden, nicht zuletzt,um die strenge Typ-Prüfung von Oberon-2 umzusetzen. OP2 verwendet hierzu die inListing 3.4 gezeigten Deklarationen.Die Fel<strong>der</strong> form und comp geben zunächst an, ob es sich um einen elementaren o<strong>der</strong>einen strukturierten Typ handelt. In mno wird die Nummer des Moduls, aus dem <strong>der</strong>Typ importiert wurde, gespeichert. Das Feld extlev gibt die Stufe <strong>der</strong> Erweiterung fürRecordtypen an; diese muß mitgeführt werden, da OP2 nur bis zu acht Erweiterungeneines Recordtyps erlaubt. 2Das Feld ref wird während des Schreibens einer Symboldatei benutzt, um festzuhalten,ob dieser Typ schon geschrieben wurde o<strong>der</strong> nicht. In sysflag werden Ausnahmen2 Man könnte in Anlehnung an Bill Gates auch sagen ”Eight extension levels should be enough foreveryone!“, wobei <strong>der</strong> Wahrheitswert dieser Aussage dahingestellt sei.


3.3 Interne Repräsentation 35Listing 3.4 OPT.Mod: Typen zur Beschreibung von TypenStruct = POINTER TO StrDesc;StrDesc = RECORDform, comp, mno, extlev: SHORTINT;ref, sysflag: INTEGER;n, size, tdadr, offset, txtpos: LONGINT;BaseTyp: Struct;link, strobj: ObjectEND;von <strong>der</strong> Oberon-2-Sprachdefinition gespeichert, die bei bestimmten Anwendungen (zumBeispiel bei <strong>der</strong> Anbindung von Prozeduren, die in C o<strong>der</strong> Modula-2 geschrieben wurden)notwendig sind.In n wird für Arraytypen die Anzahl <strong>der</strong> Elemente gespeichert, für Recordtypen dieAnzahl <strong>der</strong> an diesen Typ gebundenen Prozeduren. Das Feld size enthält die GrößedesTypsinBytes.Das Feld tdadr enthält die Adresse des Typdeskriptors für Recordtypen. DieseAdresse wird hier als Einstiegspunkt in Form eines Index in ein Array des ModulsOPL.Mod gespeichert (siehe auch Abschnitt 3.4.4).Die Verwendung des Felds offset ist unklar, da we<strong>der</strong> die Quelltexte noch die unsbekannten Quellen [6, 10, 11] darüber Auskunft geben. Anscheinend werden hier fürdynamische Arrays die Offsets zu ihren Deskriptoren gespeichert. Endgültig konntediese Frage aber lei<strong>der</strong> nicht geklärt werden.In txtpos wird die textuelle Position <strong>der</strong> Typdeklaration vermerkt, die bei spätereventuell auftretenden Fehlern wie<strong>der</strong> benötigt wird. Das Feld BaseTyp verweist fürArraytypen o<strong>der</strong> erweiterte Recordtypen auf den jeweiligen Basistyp. Über link sindFel<strong>der</strong> von Recordtypen bzw. Parameter von Prozedurtypen erreichbar. Schließlichverweist strobj noch auf eine eventuell vorhandene explizite Deklaration des Typs.Konstante WerteDie Werte anonymer und deklarierter Konstanten verschiedener Typen werden in <strong>der</strong>Symboltabelle durch die in Listing 3.5 gezeigten Typen repräsentiert.Die Fel<strong>der</strong> des Typs ConstDesc haben im einzelnen folgende Bedeutung: In extwird ein Verweis auf einen String eingetragen, sofern es sich um eine String-Konstantehandelt. Nur einen Verweis aufzunehmen hat den Vorteil, daß weniger Speicherplatzverbraucht wird, wenn wenige Strings, jedoch viele numerische Konstanten in einemModul vorkommen.In intval wird <strong>der</strong> Wert von ganzzahligen Konstanten gespeichert. Alternativ kanndieses Feld auch eine Adresse, die Größe eines Prozedurparameters, die aktuelle Positionim Quelltext o<strong>der</strong> die untere Grenze eines CASE-Labels aufnehmen. Der genaue Inhaltwird durch den Kontext, in dem <strong>der</strong> Verweis auf ConstDesc auftritt, bestimmt. Das


36 Der Oberon-2 Compiler OP2Listing 3.5 OPT.Mod: Typen zur Beschreibung verschiedener KonstantenConstExt = POINTER TO OPS.String;Const = POINTER TO ConstDesc;ConstDesc = RECORDext : ConstExt;intval : LONGINT;intval2: LONGINT;setval : SET;realval: LONGREAL;END;Feld intval2 nimmt alternativ die wirkliche Länge des in ext gespeicherten Strings, dieGröße von Prozedurvariablen o<strong>der</strong> die obere Grenze eines CASE-Labels auf.Auch das Feld setval ist mehrfach belegt: Es kann entwe<strong>der</strong> eine Mengenkonstanteaufnehmen o<strong>der</strong> verschiedene Flags“, die zum Beispiel angeben, ob eine Prozedur einen”Rumpf hat o<strong>der</strong> ob in einer CASE-Anweisung ein ELSE-Zweig vorkommt. In realvalschließlich wird <strong>der</strong> Wert von rationalen Konstanten gespeichert.SyntaxbaumWährend in den vorangehenden Abschnitten allein drei Recordtypen für die Verwaltung<strong>der</strong> Symboltabelle beschrieben werden mußten, kommt man für den Syntaxbaum mitlediglich einem Recordtyp aus, <strong>der</strong>, wie in Listing 3.6 gezeigt, aufgebaut ist.Listing 3.6 OPT.Mod: Typen zur Beschreibung des SyntaxbaumsNode = POINTER TO NodeDesc;NodeDesc = RECORDleft, right, link: Node;class, subcl: SHORTINT;readonly: BOOLEAN;typ: Struct;obj: Object;conval: ConstEND;Die Verkettung einzelner Anweisungen und Ausdrücke zu komplexeren Anweisungsfolgenund Ausdrücken wird durch die Fel<strong>der</strong> left und right sowie in manchen Fällendas Feld link ermöglicht. Grundsätzlich lassen sich die meisten Konstrukte von Oberon-2 in zwei Teile aufglie<strong>der</strong>n. Zum Beispiel bestehen sowohl eine Zuweisung als auch eineWHILE-Schleife aus zwei Teilen: Die Zuweisung aus einer Variablen, an die zugewiesenwerden soll, und einem Ausdruck, die WHILE-Schleife aus einer Bedingung und einer(eventuell zusammengesetzten) Anweisung. Solche Aufteilungen werden durch die Fel<strong>der</strong>


3.3 Interne Repräsentation 37left und right verkettet. An einige Stellen werden aber in Oberon-2 Folgen benötigt(etwa Anweisungsfolgen o<strong>der</strong> Parameterlisten), <strong>der</strong>en Reihenfolge erhalten bleiben muß;diese werden durch das Feld link verkettet.Die Fel<strong>der</strong> class und subcl beschreiben, um welche Art von Knoten es sich genauhandelt. Dabei wird grob zwischen Anweisungen (zum Beispiel WHILE-Schleifen), Operationen(zum Beispiel dem +-Operator) und Ausdrücken (zum Beispiel a[i]) unterschieden.Da viele Arten von Knoten in verschiedenen, teils unerwarteten Zusammenhängenvorkommen, sind beide Fel<strong>der</strong> nötig, um die genaue Art des Knotens zu bestimmen.Beispielsweise sind die vordeklarierten Prozeduren INC(), INCL() und NEW() jeweilsZuweisungen, für die allerdings ein sehr unterschiedlicher Code erzeugt werden muß.Das Feld readonly gibt für einen Ausdruck an, ob dieser schreibgeschützt ist. DerTyp eines Ausdrucks wird in typ verwaltet. Für explizit deklarierte Bezeichner verweistobj auf den entsprechenden Eintrag in <strong>der</strong> Symboltabelle. Konstante Werte werdenschließlich in conval gespeichert.3.3.2 Verwaltung <strong>der</strong> Symboltabelle (OPT.Mod)Die Verwaltung <strong>der</strong> Symboltabelle wird — neben einer Prozedur, die den Syntaxbaumbetrifft — durch das Modul OPT.Mod implementiert. Die Schnittstelle des Moduls ist,wie in Listing 3.7 gezeigt, aufgebaut.Die Konstante MaxConstLen wird lediglich im Parser verwendet, um die maximaleGröße von Code-Prozeduren zu bestimmen. 3 Die danach deklarierten KonstantenHdPtrName, HdProcName und HdTProcName enthalten Namen, die für bestimmte (nichtexportierte, aber dennoch intern wichtige) Konstrukte automatisch durch den Compilererzeugt werden.Die Variable topScope verweist während <strong>der</strong> Übersetzung eines Moduls immer aufden gerade gültigen Sichtbarkeitsbereich. Die einzelnen Sichtbarkeitsbereiche werdennach dem LIFO-Prinzip verwaltet. In SYSimported wird festgehalten, ob das zu übersetzendeModul das Pseudo-Modul SYSTEM importiert und damit als nicht portabel markiertist. Die folgenden Variablen undftyp, bytetyp usw. enthalten Beschreibungen <strong>der</strong>vordeklarierten Typen von Oberon-2. In nofGmod wird die Anzahl <strong>der</strong> importiertenModule gespeichert, während das Array GlbMod Verweise auf die entsprechenden Sichtbarkeitsbereicheenthält.Die Prozeduren Init() und Close() werden bei Beginn und am Ende eines Übersetzungsvorgangsaufgerufen, um bestimmte Vorbedingungen innerhalb <strong>der</strong> Symboltabellezu etablieren. <strong>Zur</strong> einfacheren Erzeugung neuer Objekte <strong>der</strong> Symboltabelle bzw. desSyntaxbaums dienen die Prozeduren NewConst(), NewObj(), NewStr(), NewNode() undNewExt(). Mittels <strong>der</strong> Prozeduren FindImport(), Find() bzw. FindField() könnenBezeichner aus importierten Modulen, dem aktuellen Modul bzw. einem Recordtyp lokalisiertwerden; die beiden ersteren suchen immer nach dem in OPS.name angegebenen3 Solche Prozeduren werden durch den Marker - nach dem Schlüsselwort PROCEDURE gekennzeichnet.Durch sie können wir Maschinencode direkt in Oberon-2-Programme einfügen. Allerdings dürfen sieausschließlich innerhalb von nicht portablen Modulen verwendet werden: Das Pseudo-Modul SYSTEMmuß importiert werden.


38 Der Oberon-2 Compiler OP2Listing 3.7 Schnittstelle von OPT.ModDEFINITION OPT;IMPORT OPS;CONST MaxConstLen = OPS.MaxStrLen;HdPtrName = "@ptr"; HdProcName = "@proc"; HdTProcName = "@tproc";TYPE (* types Object/Desc, Struct/Desc, Const/Ext/Desc and Node/Descdiscussed seperately *)VAR topScope: Object; SYSimported: BOOLEAN;undftyp, bytetyp, booltyp, chartyp, sinttyp, inttyp, linttyp,realtyp, lrltyp, settyp, stringtyp, niltyp, notyp, sysptrtyp: Struct;nofGmod: SHORTINT; GlbMod: ARRAY maxImps OF Object;PROCEDURE Init;PROCEDURE Close;PROCEDURE NewConst(): Const;PROCEDURE NewObj(): Object;PROCEDURE NewStr(form, comp: SHORTINT): Struct;PROCEDURE NewNode(class: SHORTINT): Node;PROCEDURE NewExt(): ConstExt;PROCEDURE FindImport(mod: Object; VAR res: Object);PROCEDURE Find(VAR res: Object);PROCEDURE FindField(name: OPS.Name; typ: Struct; VAR res: Object);PROCEDURE Insert(name: OPS.Name; VAR obj: Object);PROCEDURE OpenScope(level: SHORTINT; owner: Object);PROCEDURE CloseScope;PROCEDURE Import(VAR aliasName, impName, selfName: OPS.Name);PROCEDURE Export(VAR modName: OPS.Name; VAR newSF: BOOLEAN; VAR key: LONGINT);END OPT.aktuellen Bezeichner. Ein neuer Bezeichner wird durch die Prozedur Insert() in denaktuellen Sichtbarkeitsbereich eingetragen.Die Prozeduren OpenScope() und CloseScope() werden für jeden neuen Sichtbarkeitsbereichentsprechend aufgerufen. Schließlich wird durch die Prozeduren Import()bzw. Export() die Verwaltung <strong>der</strong> Symboldateien implementiert (siehe auch Abschnitt3.3.4).Interne Bezeichner: Einige Bezeichner, die für die folgenden Kapitel wichtig sind,werden von OPT.Mod nicht exportiert und treten deswegen in Listing 3.7 nicht auf. Wirwollen sie hier aber dennoch kurz beschreiben.Der jedes Modul umgebende Sichtbarkeitsbereich, in dem die vordeklarierten Bezeichner”deklariert“ sind, wird in <strong>der</strong> Variable universe gespeichert, <strong>der</strong> Sichtbarkeitsbereichdes internen Pseudo-Moduls SYSTEM in <strong>der</strong> Variable syslink. Die Werte fürbestimmte Fel<strong>der</strong> <strong>der</strong> Recordtypen ObjDesc, StrDesc, ConstDesc und NodeDesc sindals Konstanten deklariert.3.3.3 Generierung des Syntaxbaums (OPB.Mod)Die Verwaltung des Syntaxbaums wird durch das Modul OPB.Mod implementiert. DieSchnittstelle des Moduls ist, wie in Listing 3.8 gezeigt, aufgebaut.


3.3 Interne Repräsentation 39Listing 3.8 Schnittstelle von OPB.ModDEFINITION OPB;IMPORT OPT, OPS;VAR typSize: PROCEDURE(typ: OPT.Struct; allocDesc: BOOLEAN);PROCEDURE NewLeaf(obj: OPT.Object): OPT.Node;PROCEDURE NewBoolConst(boolval: BOOLEAN): OPT.Node;PROCEDURE NewIntConst(intval: LONGINT): OPT.Node;PROCEDURE NewRealConst(realval: LONGREAL; typ: OPT.Struct): OPT.Node;PROCEDURE NewString(VAR str: OPS.String; len: LONGINT): OPT.Node;PROCEDURE Nil(): OPT.Node;PROCEDURE EmptySet(): OPT.Node;PROCEDURE Construct(class: SHORTINT; VAR x: OPT.Node; y: OPT.Node);PROCEDURE Link(VAR x, last: OPT.Node; y: OPT.Node);PROCEDURE DeRef(VAR x: OPT.Node);PROCEDURE Index(VAR x: OPT.Node; y: OPT.Node);PROCEDURE Field(VAR x: OPT.Node; y: OPT.Object);PROCEDURE OptIf(VAR x: OPT.Node);PROCEDURE SetRange(VAR x: OPT.Node; y: OPT.Node);PROCEDURE SetElem(VAR x: OPT.Node);PROCEDURE In(VAR x: OPT.Node; y: OPT.Node);PROCEDURE TypTest(VAR x: OPT.Node; obj: OPT.Object; guard: BOOLEAN);PROCEDURE StFct(VAR par0: OPT.Node; fctno: SHORTINT; parno: INTEGER);PROCEDURE StPar0(VAR par0: OPT.Node; fctno: INTEGER);PROCEDURE StPar1(VAR par0: OPT.Node; x: OPT.Node; fctno: SHORTINT);PROCEDURE StParN(VAR par0: OPT.Node; x: OPT.Node; fctno, n: INTEGER);PROCEDURE CheckParameters(fp, ap: OPT.Object; checkNames: BOOLEAN);PROCEDURE Param(ap: OPT.Node; fp: OPT.Object);PROCEDURE PrepCall(VAR x: OPT.Node; VAR fpar: OPT.Object);PROCEDURE Call(VAR x: OPT.Node; apar: OPT.Node; fp: OPT.Object);PROCEDURE StaticLink(dlev: SHORTINT);PROCEDURE Enter(VAR procdec: OPT.Node; stat: OPT.Node; proc: OPT.Object);PROCEDURE Return(VAR x: OPT.Node; proc: OPT.Object);PROCEDURE Assign(VAR x: OPT.Node; y: OPT.Node);PROCEDURE MOp(op: SHORTINT; VAR x: OPT.Node);PROCEDURE Op(op: SHORTINT; VAR x: OPT.Node; y: OPT.Node);PROCEDURE Inittd(VAR inittd, last: OPT.Node; typ: OPT.Struct);END OPB.In die Prozedurvariable typSize wird durch das Backend eine für die Plattformpassende Prozedur zur Berechnung <strong>der</strong> Größe eines Typs eingetragen.Die Prozeduren NewLeaf(), NewBoolConst(), NewIntConst(), NewRealConst() undNewString() dienen — wie auch im Modul OPT.Mod — <strong>der</strong> einfacheren Erzeugung bestimmterhäufig verwendeter Knoten. Den gleichen Zweck haben auch die ProzedurenNil() und EmptySet().Durch die Prozedur Construct() werden die Knoten für Anweisungen aufgebaut:Es wird ein Knoten des Typs class erzeugt, dessen linker bzw. rechter Sohn x bzw.y sind. Der neue Knoten wird dann in x zurückgegeben. Verkettungen über das FeldNode.link werden dagegen von <strong>der</strong> Prozedur Link() durchgeführt.


40 Der Oberon-2 Compiler OP2Die Prozeduren DeRef(), Index() bzw. Field() erzeugen Knoten für die Dereferenzierungvon Zeigern, den Zugriff auf Elemente eines Arrays bzw. den Zugriff auf dieElemente eines Records. Der in x übergebene Knoten wird dabei an einen neuen Knotenangehängt, <strong>der</strong> wie<strong>der</strong>um in x zurückgegeben wird.In OptIf() werden einige Optimierungen an dem für eine komplexe IF-Anweisungerzeugten Syntaxbaum durchgeführt.Für Konstanten und Literale vom Typ SET werden durch die Prozeduren SetRange()und SetElem() die entsprechenden Knoten erzeugt bzw. verän<strong>der</strong>t. Für konstanteWerte sind zum Beispiel keine Berechnungen nötig und die Menge kann direkt im FeldNodeDesc.conval.setval gespeichert werden. Der Operator IN für Mengen wird durchdie Prozedur In() behandelt.Die Prozedur TypTest() erzeugt Knoten für Typ-Zusicherungen und Typ-Prüfungen:x muß auf eine Variable verweisen, obj auf einen Typ. Der Parameter guard entscheidet,ob eine Zusicherung (guard = TRUE) o<strong>der</strong>einePrüfung (guard = FALSE) erzeugtwerden soll.Vordeklarierte Prozeduren und Funktionsprozeduren (und solche aus dem Pseudo-Modul SYSTEM) werden durch StFct(), StPar0(), StPar1() und StParN() behandelt.Erstere erzeugt dabei die endgültigen Knoten, während die restlichen drei den korrektenAufbau <strong>der</strong> Parameterlisten überprüfen. Dies ist nötig, da einige <strong>der</strong> vordeklariertenProzeduren und Funktionsprozeduren von den üblichen Regeln in Oberon-2 abweichen.Die Prozeduren CheckParameters() und Param() dienen zur Überprüfung <strong>der</strong> Kompatibilitätvon formalen und aktuellen Parametern. Während erstere dies ausschließlichauf <strong>der</strong> Basis von Typen tut, werden in letzterer die Son<strong>der</strong>regeln <strong>der</strong> Sprachdefinitionbehandelt. 4Vor einem Prozeduraufruf werden durch PrepCall() einige Prüfungen bezüglich typgebundenerProzeduren durchgeführt. Prozeduraufrufe selbst werden in Call() behandelt,wobei für typgebundene Prozeduren die Liste <strong>der</strong> aktuellen Parameter um denversteckten Empfängerparameter erweitert wird. Durch StaticLink() werden in denbetroffenen Sichtbarkeitsbereichen Flags gesetzt, die den Bedarf einer statischen Verkettung<strong>der</strong> Aktivierungsrecords auf dem Stack anzeigen. Die vor Eintritt in eine Prozedurnotwendigen Maßnahmen werden durch den von <strong>der</strong> Prozedur Enter() erzeugten Knotenrepräsentiert. Umgekehrt wird für jede RETURN-Anweisung in einer Prozedur durch denAufruf von Return() ein entsprechen<strong>der</strong> Knoten erzeugt. Aus unbekannten Gründenweicht OP2 hier übrigens von <strong>der</strong> Sprachdefinition ab: RETURN-Anweisungen sind auchim BEGIN-Teil eines Moduls erlaubt.Zuweisungen an Variable und Parameter werden in <strong>der</strong> Prozedur Assign() behandelt.Für Recordtypen, die als Referenzparameter übergeben werden, wird hier die fürZuweisungen an sie nötige, implizite Typ-Zusicherung erzeugt. 54 Die Prozedur CheckParameters() wird nicht nur beim Aufruf einer Prozedur, son<strong>der</strong>n auch beieiner Zuweisung an eine Prozedurvariable verwendet.5 Eine Prozedur, die einen Referenzparameter vom Typ A erwartet, kann an dessen Position in <strong>der</strong>Parameterliste auch eine Variable vom Typ B akzeptieren, <strong>der</strong> von A abgeleitet wurde. Würde nuninnerhalb <strong>der</strong> Prozedur eine an<strong>der</strong>e Variable vom Typ A an diesen Parameter zugewiesen, so würde dieSemantik <strong>der</strong> Zuweisung verletzt, obwohl die Zuweisung sonst statisch korrekt wäre.


3.3 Interne Repräsentation 41Knoten für monadische bzw. dyadische Operatoren werden durch die ProzedurenMOp() bzw. Op() erzeugt; konstante (Teil-)Ausdrücke werden in <strong>der</strong> internen ProzedurConstOp() behandelt.Die Prozedur Inittd() schließlich scheint in <strong>der</strong> hier beschriebenen Version vonOP2 keine Funktion mehr zu haben. Sie wird zwar im Parser aufgerufen, aber die damiterzeugte Liste wird nicht weiter verwendet. Im Traversierer ist das CASE-Label für denentsprechenden Knoten leer. Die Typdeskriptoren werden in <strong>der</strong> hier beschriebenenVersion während des Traversierens <strong>der</strong> Symboltabelle erzeugt.3.3.4 Aufbau <strong>der</strong> SymboldateienSymboldateien gehören zwar streng genommen nicht zur internen Repräsentation einesModuls, aber da wir später für <strong>Fro<strong>der</strong>on</strong>-1 auch Än<strong>der</strong>ungen in den Symboldateienvornehmen müssen, wollen wir hier dennoch kurz ihren Aufbau beschreiben.Eine Symboldatei enthält die Schnittstelle eines Moduls in kompakter Form. Alleexportierten Bezeichner und <strong>der</strong>en Typen werden hier — zusammen mit einigen <strong>weiteren</strong>Informationen — so gespeichert, daß die Schnittstelle eines importierten Modulsinnerhalb des Compilers leicht rekonstruiert werden kann.Zum Aufbau von Symboldateien für Sprachen wie Oberon-2 wurden eine Reihe vonunterschiedlichen Verfahren publiziert. Wir wollen auf diese hier nicht weiter eingehenund uns lediglich auf eine Beschreibung des Formats beschränken, welches <strong>der</strong> in dieserArbeit verwendete Compiler benutzt. Den interessierten Leser verweisen wir aber fürweitere Informationen auf die grundlegenden Darstellungen in [64] und [65] sowie aufdie experimentellen Varianten in [28] und [11]. Die hier von uns gegebene Beschreibungbasiert auf dem Studium des Quelltexts und den Arbeiten [60, 65].Der Aufbau einer Symboldatei des hier verwendeten Compilers ist in Listing 3.9 beschrieben.Es handelt sich bei dieser Beschreibung um eine Variante <strong>der</strong> EBNF-Notation.Non-Terminalsymbole sind kursiv gesetzt. Großgeschriebene Terminalsymbole stellentags dar, die zur Trennung <strong>der</strong> verschiedenen Teile einer Symboldatei verwendet werden;ihre Bedeutung ist aus Tabelle 3.1 zu entnehmen (dort werden auch die tags UNDEF undNOTYP erwähnt, die aber üblicherweise nicht in den Symboldateien auftreten). Terminalsymbolein Kleinschreibung stellen die eigentlichen Daten <strong>der</strong> Symboldatei dar. Füreinige ist ihre Länge in Bytes hinter einem ”:“ angegeben; solche ohne Längenangabewerden entwe<strong>der</strong> durch ein Endekennzeichen o<strong>der</strong> durch ein Stopbit abgeschlossen. Alletags sind ein Byte lang.


42 Der Oberon-2 Compiler OP2Listing 3.9 Aufbau von SymboldateienSymbolFile = SFTAG ModuleAnchor {Element }.ModuleAnchor = MOD key name.Element = ModuleAnchor| CON Constant| (TYPE | HDTYPE) Type| (VAR | RDVAR | FLD | RDFLD ) Variable| (VALPAR | VARPAR) Parameter| PLIST {Element } (XPRO | IPRO | TPRO ref:1 mno:1) Procedure| PLIST {Element } TPRO TProcedure| PLIST {Element } CPRO CProcedure| PTR PointerType| PLIST {Element } PROC ProcType| ARR ArrayType| DARR DynArrayType| FLIST {Element } REC RecordType| (HDPTR | HDPROC) HiddenFldOff| HDTPRO ref:1 mthdno:1 procno:1| FIX Fixup| NTPRO ref:1 n:1| SYS Flag.Constant = (BYTE|CHAR|SINT) value:1 name| BOOL (FALSE | TRUE ) name| (INT | LINT | SET ) value name| REAL value:4 name| LREAL value:8 name| STRING name name| NIL name.Type = ref:1 modno:1 name.Variable = ref:1 offset name.Parameter = ref:1 offset name.Procedure = ref:1 procno:1 name.CProcedure = ref:1 len:1 {code:1} name.TProcedure = ref:1 mthdno:1 procno:1 name.PointerType = baseRef:1 modno:1ProcType = resultRef:1 modno:1ArrayType = elemRef:1 modno:1 size boundAdr nofElem.DynArrayType = elemRef:1 modno:1 size lenOff.RecordType = baseRef:1 modno:1 size descAdr.HiddenFldOff = offset.Fixup = ptrRef:1 baseRef:1Flag = ref:1 value.


3.3 Interne Repräsentation 43Tag Wert BemerkungSFTAG 0FAX Kennung einer SymboldateiCON 1 exportierte KonstanteTYPE 2 exportierter TypHDTYPE 3 nicht-exportierter TypVAR 4 exportierte VariableXPRO 5 exportierte ProzedurIPRO 6 Interrupt-ProzedurCPRO 7 Code-ProzedurPTR 8 (exportierter) ZeigertypPROC 9 (exportierter) ProzedurtypARR 10 (exportierter) ArraytypDARR 11 (exportierter) dynamischer ArraytypREC 12 (exportierter) RecordtypPLIST 13 Liste formaler ParameterVALPAR 14 WertparameterVARPAR 15 ReferenzparameterFLIST 16 Liste von Recordfel<strong>der</strong>nFLD 17 RecordfeldHDPTR 18 nicht-exportiertes Zeigerfeld eines RecordsHDPROC 19 nicht-exportiertes Prozedurfeld eines RecordsFIX 20 Fixup für vorwärtsdeklarierte ZeigertypenSYS 21 Plattform-spezifische FlagsMOD 22 ModulRDVAR 23 schreibgeschützte VariableRDFLD 24 schreibgeschütztes RecordfeldTPRO 25 exportierte typgebundene ProzedurNTPRO 26 Anzahl typegebundener Prozeduren eines RecordtypsHDTPRO 27 nicht-exportierte typgebundene ProzedurVordeklarierte Konstante Wert BemerkungFALSE 0X —TRUE 1X —Vordeklarierter Typ Wert BemerkungUNDEF 0 (noch) undefinierter TypBYTE 1 SYSTEM.BYTEBOOL 2 BOOLEANCHAR 3 CHARSINT 4 SHORTINTINT 5 INTEGERLINT 6 LONGINTREAL 7 REALLREAL 8 LONGREALSET 9 SETSTRING 10 Typ einer String-KonstanteNIL 11 Typ einer Konstante für NILNOTYP 12 nicht getypt (z.B. vordeklarierte Prozeduren)Tabelle 3.1: In Symboldateien verwendete tags


44 Der Oberon-2 Compiler OP23.4 Generierung von ObjektcodeDie Codegenerierung wird in OP2 durch das Modul OPV.Mod gesteuert, welches den vomFrontend erzeugten Syntaxbaum traversiert und Prozeduren aus den Modulen OPC.Modund OPL.Mod aufruft. Diese Prozeduren erzeugen dann die jeweiligen Instruktionen füreinen bestimmten Prozessor bzw. eine bestimmte Plattform. Wie schon in Abschnitt3.3 beschäftigen wir uns auch hier zunächst mit den verwendeten Datentypen bzw. Datenstrukturen,bevor wir auf die Module selbst eingehen.3.4.1 Datentypen (OPL.Mod)<strong>Zur</strong> Generierung von effizientem Code ist es notwendig, die Ausgabe einer bestimmtenFolge von Instruktionen solange zu verzögern, bis lokal feststeht, daß keine effizientererFolge für eine gewisse Aufgabe gewählt werden kann. Der in Listing 3.10 gezeigte TypItem dient genau diesem Zweck.Listing 3.10 OPL.Mod: Typ zur Beschreibung von ItemsItem = RECORDmode: INTEGER;typ: OPT.Struct;reg: INTEGER;bd: LONGINT;inxReg: INTEGER;xsize: INTEGER;scale: INTEGER;tJump, fJump: Label;offsReg: INTEGER;nolen: INTEGEREND;Parameter vom Typ Item werden während <strong>der</strong> Codegenerierung zwischen den Prozedurendes Backends ausgetauscht. In ihnen werden die zu verwendenden Adressierungsmodiund Register so lange gespeichert, bis die für sie passenden Instruktionen generiertwerden können. Da <strong>der</strong> Syntaxbaum so traversiert wird, daß zunächst die ”Kin<strong>der</strong>“ einesKnotens behandelt werden, bevor er selbst behandelt wird (post-or<strong>der</strong> traversal), erreichtman eine kontext-freie Generierung des Objektcodes (siehe auch [26, Seite 113ff]). Derfür einen Knoten K erzeugte Code C(K) hängt lediglich von dem für seine NachkommenK i erzeugten Code C(K i )ab: C(K) =F K (C(K 1 ),...,C(K n )). Der Einfluß <strong>der</strong>Symboltabelle wird hier allerdings außer acht gelassen.Als Beispiel betrachten wir die Generierung von Code für eine Zuweisung. Hier werdenzunächst durch Aufruf <strong>der</strong> internen Prozeduren OPV.Expr() und OPV.Designator()aus den Knoten des Ausdrucks und des Ziels <strong>der</strong> Zuweisung Items erzeugt, die den fürsie generierten Code repräsentieren. Die entsprechenden Code-Stücke für die Auswertungdes Ausdrucks und des Ziels (genauer: <strong>der</strong> Adresse des Ziels) werden dabei als


3.4 Generierung von Objektcode 45Seiteneffekt dieser Prozeduren generiert. Erst jetzt wird durch den Aufruf <strong>der</strong> internenProzedur OPV.Assign() <strong>der</strong> Code für die Zuweisung selbst generiert.Die einzelnen Fel<strong>der</strong> des Typs Item haben die folgenden Bedeutungen: In mode wirdzunächst <strong>der</strong> zu verwendende Adressierungsmodus gespeichert; bei dem hier betrachtetenCode-Generator für den Motorola-MC68020-Prozessor also zum Beispiel Register,Register-Indirekt o<strong>der</strong> Register-Postinkrement. Details zu diesen Adressierungsarten undden Prozessoren <strong>der</strong> Motorola-M68000-Familie finden sich in [33, 35, 48].Das Feld typ verweist auf den Typ dieses Items, <strong>der</strong> zum Beispiel bei <strong>der</strong> Generierungvon Code für numerische Operationen benötigt wird. In reg wird das Registergespeichert, in dem zum Beispiel ein temporäres Ergebnis abgelegt wurde.Für unterschiedlichste Zwecke wird das Feld bd verwendet. Bei Verwendung einerindizierten Adressierungsart wird hier das jeweilige Basisregister gespeichert. Bei <strong>der</strong>Auswertung Boolscher Ausdrücke werden die Bedingungen für die jeweiligen Sprünge(siehe unten) hier abgelegt. Schließlich enthält es für konstante Adressierungsarten (immediatemode) die jeweilige Konstante und für absolute Adressierungsarten die ModulundEintragsnummer des betroffenen Objekts.In inxReg wird bei indizierter Adressierung das verwendete Index-Register gespeichert.Die Fel<strong>der</strong> xsize und scale stehen in diesem Fall für die Größe <strong>der</strong> indiziertenElemente.Die Fel<strong>der</strong> tJump und fJump werden für die Generierung von Instruktionen zur BehandlungBoolscher Ausdrücke benötigt, da diese laut Sprachdefinition [46] in Kurzschlußformausgewertet werden müssen. Zum Beispiel wird <strong>der</strong> Ausdruck a & b in <strong>der</strong>Formresult := IF a THEN RETURN b ELSE RETURN FALSEausgewertet. 6 Der generierte Code enthält deswegen Sprunganweisungen, <strong>der</strong>en Zieladressenhier verwaltet werden.Das Feld offsReg enthält für dynamische und offene Arrays das Register, welchesauf den entsprechenden Deskriptor verweist. In nolen werden schließlich für dynamischeArrays die Anzahl <strong>der</strong> Dimensionen und für Strings ihre Länge gespeichert.3.4.2 Traversieren des Syntaxbaums (OPV.Mod)Die oben schon angesprochene Traversierung des Syntaxbaums übernimmt das ModulOPV.Mod. Daneben wird in diesem Modul auch die Symboltabelle mit den für einebestimmte Plattform relevanten Werten dekoriert, zum Beispiel um die Größe eines Typszu bestimmen. Die Schnittstelle des Moduls ist, wie in Listing 3.11 gezeigt, aufgebaut.Die Prozedur Init() wird vor <strong>der</strong> Übersetzung eines Moduls aufgerufen, um bestimmteOptionen von <strong>der</strong> Benutzerschnittstelle an das Backend weiterzugeben.Durch die Benutzerschnittstelle Compiler.Mod wird die Prozedur TypSize() ausdiesem Modul in die entsprechende Prozedurvariable des Moduls OPB.Mod gespeichert.6 Dies stellt natürlich kein gültiges Oberon-2-Konstrukt dar, son<strong>der</strong>n soll nur zur Illustration <strong>der</strong>Kurzschlußform dienen.


46 Der Oberon-2 Compiler OP2Listing 3.11 Schnittstelle von OPV.ModDEFINITION OPV;IMPORT OPT, OPM;PROCEDURE Init(opt: SET; bpc: LONGINT);PROCEDURE TypSize(typ: OPT.Struct; dummy: BOOLEAN);PROCEDURE AdrAndSize;PROCEDURE Module(prog: OPT.Node);END OPV.Die Prozedur dient zur Berechnung <strong>der</strong> Größe eines Typs (und <strong>der</strong> Typen, die Teil seinerDeklaration sind) und zur Vergabe von Offsets für Recordfel<strong>der</strong>. Der Parameter dummyhat in <strong>der</strong> hier besprochenden Version von OP2 keine Bedeutung.Die restlichen Adressen und Größen (zum Beispiel für Prozeduren und <strong>der</strong>en Parameter)werden von Compiler.Mod aus durch einen Aufruf <strong>der</strong> Prozedur AdrAndSize()berechnet. Diese traversiert die gesamte Symboltabelle (nicht nur einzelne Typen) undberechnet die entsprechenden Informationen.Schließlich wird in <strong>der</strong> Prozedur Module() die eigentliche Traversierung des vomFrontend erzeugten Syntaxbaums gestartet.3.4.3 High-level Codegenerator (OPC.Mod)Die zweite Stufe <strong>der</strong> Codegenerierung wird in OP2 durch das Modul OPC.Mod realisiert,dessen Schnittstelle, wie in Listing 3.12 und Listing 3.13 gezeigt, aufgebaut ist.Auf dieser Stufe werden Prozeduren deklariert, die für bestimmte Konstrukte (o<strong>der</strong>Teile von diesen) von Oberon-2 Code generieren. Die endgültige Umsetzung in Instruktionenfür den jeweiligen Prozessor wird aber erst in OPL.Mod erledigt.Die Variable saveRegs wird lediglich in <strong>der</strong> Prozedur DeRef() benutzt, um festzustellen,ob Adreßregister wie<strong>der</strong>verwendet werden sollen. Sie wird während <strong>der</strong> Traversierungan entsprechenden Stellen durch das Modul OPV.Mod gesetzt.Die Prozedur Init() wird vor <strong>der</strong> Übersetzung eines Moduls aufgerufen, um bestimmteOptionen von <strong>der</strong> Benutzerschnittstelle an das Backend weiterzugeben.Durch MakeLen() wird aus einem Item für ein dynamisches Array ein Item erzeugt,das die Länge <strong>der</strong> n-ten Dimension darstellt. Mittels MakeIntConst() wird ein Itemfür eine ganzzahlige Konstante erzeugt. Die Prozedur MakeVar() erzeugt ein Item füreine Variable, die in <strong>der</strong> Symboltabelle gespeichert ist. Hierbei wird auch das eventuellnötige Basisregister für Zugriffe auf diese Variable bestimmt, was es nötig machen kann,den statischen Links auf dem Stack zu folgen.<strong>Zur</strong> Dereferenzierung eines Zeigers wird mittels DeRef() ein entsprechendes Item generiert.Durch die Prozedur StaticTag() wird ein Item für den statischen Typdeskriptoreines Typs erzeugt. Ein entsprechendes Item für den dynamischen Typdeskriptoreiner Zeigervariablen o<strong>der</strong> eines Referenzparameters erzeugt die Prozedur MakeTag().Items zur Beschreibung von Konstanten werden durch die Prozedur MakeConst()erzeugt. Sie verwendet OPL.AllocConst() um die Konstante in dem entsprechendenBereich <strong>der</strong> Objektdatei abzulegen.


3.4 Generierung von Objektcode 47Listing 3.12 Schnittstelle von OPC.Mod (Teil 1)DEFINITION OPC;IMPORT OPT, OPL;VAR saveRegs: BOOLEAN;PROCEDURE Init(options: SET);PROCEDURE MakeLen(VAR arr: OPL.Item; n: LONGINT; VAR item: OPL.Item);PROCEDURE MakeIntConst(val: LONGINT; typ: OPT.Struct; VAR item: OPL.Item);PROCEDURE MakeVar(obj: OPT.Object; VAR item: OPL.Item);PROCEDURE DeRef(typ: OPT.Struct; VAR item: OPL.Item);PROCEDURE StaticTag(typ: OPT.Struct; VAR tag: OPL.Item);PROCEDURE MakeTag(obj: OPT.Object; typ: OPT.Struct; VAR item, tag: OPL.Item);PROCEDURE MakeConst(obj: OPT.Object; const: OPT.Const; typ: OPT.Struct;VAR item: OPL.Item);PROCEDURE SetElem(VAR item: OPL.Item);PROCEDURE Convert(VAR source: OPL.Item; desttyp: OPT.Struct);PROCEDURE GetDynArrVal(VAR item: OPL.Item);PROCEDURE MakeField(VAR item: OPL.Item; offset: LONGINT; typ: OPT.Struct);PROCEDURE MakeIndex(VAR index, res: OPL.Item);PROCEDURE MakeProc(obj: OPT.Object; subcl: SHORTINT; VAR item: OPL.Item);PROCEDURE MakeCocItem(trueCond: INTEGER; VAR res: OPL.Item);PROCEDURE MakeFCocItem(trueCond: INTEGER; VAR res: OPL.Item);PROCEDURE PushRegs(regs: SET);PROCEDURE PopRegs(regs: SET);PROCEDURE TrueJump(VAR expression: OPL.Item; VAR label: OPL.Label);PROCEDURE FalseJump(VAR expression: OPL.Item; VAR label: OPL.Label);PROCEDURE Assign(VAR source, dest: OPL.Item);PROCEDURE MoveDynArrStack(formalTyp: OPT.Struct; offset: LONGINT;VAR item: OPL.Item);PROCEDURE MoveAdrStack(offset: LONGINT; VAR item: OPL.Item);PROCEDURE MoveStack(offset: LONGINT; VAR item: OPL.Item);PROCEDURE Copy(VAR source, dest: OPL.Item);PROCEDURE Decrement(VAR designator, expression: OPL.Item);PROCEDURE Increment(VAR designator, expression: OPL.Item);PROCEDURE Include(VAR set, element: OPL.Item);PROCEDURE Exclude(VAR set, element: OPL.Item);PROCEDURE EnterMod;PROCEDURE EnterProc(proc: OPT.Object);PROCEDURE Return(proc: OPT.Object; withRes: BOOLEAN; VAR result: OPL.Item);PROCEDURE WriteStaticLink(obj: OPT.Object);PROCEDURE Call(VAR item: OPL.Item; obj: OPT.Object);PROCEDURE GetResult(typ: OPT.Struct; VAR res: OPL.Item);PROCEDURE TypeTest(VAR item: OPL.Item; typ: OPT.Struct; guard, equal: BOOLEAN);PROCEDURE Case(VAR expression: OPL.Item; lo, hi: LONGINT; VAR label: OPL.Label;VAR jtAdr: LONGINT);PROCEDURE AddToSP(data: LONGINT);Die Prozedur SetElem() erzeugt aus einem Item, das einen ganzzahligen Wert beschreibt,ein entsprechendes Item, das die Menge beschreibt, die diesen Wert enthält.Die von Oberon-2 erlaubten Umwandlungen von Typen werden durch die ProzedurConvert() erledigt. Ein Item, das den Wert eines dynamischen Arrays beschreibt, erzeugtdie Prozedur GetDynArrVal().


48 Der Oberon-2 Compiler OP2Für den Zugriff auf Recordfel<strong>der</strong> erzeugt MakeField() ein passendes Item. Die entsprechendeAufgabe für Zugriffe auf Arrays übernimmt MakeIndex(). MakeProc() generiertein Item, das die Adresse einer Prozedur beschreibt. Hier werden auch die Instruktionenerzeugt, um die Adresse einer typgebundenen Prozedur zur Laufzeit aus demjeweiligen Typdeskriptor zu holen. Für Bedingungen werden mit MakeCocItem() bzw.MakeFCocItem() (für Gleitkommabedingungen) die passenden Items erzeugt.Die Prozeduren PushRegs() und PopRegs() sichern die übergebenen Register aufdem Stack bzw. stellen <strong>der</strong>en Werte wie<strong>der</strong> her.Instruktionen für bedingte Sprünge werden durch TrueJump() und FalseJump()generiert. Die Prozeduren unterscheiden sich lediglich darin, ob <strong>der</strong> Sprung für einewahre o<strong>der</strong> eine falsche Bedingung ausgeführt wird.Die für eine Zuweisung nötigen Instruktionen werden durch die Prozedur Assign()erzeugt. Während für elementare Typen einfache Instruktionen generiert werden können,müssen für strukturierte Typen entsprechende Schleifen erzeugt werden.Eintragungen auf dem Stack, die zum Beispiel für die Übergabe von Parameternbenutzt werden, können durch die Prozeduren MoveDynArrStack() (für dynamischeArrayparameter), MoveAdrStack() (für Referenzparameter) und MoveStack() (für allean<strong>der</strong>en Parameter) erfolgen.Die Prozeduren Copy(), Decrement(), Increment(), Include() und Exclude()erzeugen den Code für die vordeklarierten Prozeduren COPY(), INC(), DEC(), INCL()und EXCL().Die Prozeduren EnterMod() und EnterProc() erzeugen den für den Eintritt in einModul bzw. eine Prozedur notwendigen Code. Symmetrisch dazu erzeugt die ProzedurReturn() den zum Verlassen einer Prozedur notwendigen Code. Abweichend von <strong>der</strong>Sprachdefinition wird hier auch Code für das Verlassen eines Moduls durch die AnweisungRETURN erzeugt.Der zum Zugriff auf Variablen in äußeren Sichtbarkeitsbereichen nötige static linkwird durch WriteStaticLink() erzeugt; wenn nötig wird auch Code zum Durchlaufen<strong>der</strong> static links erzeugt.Instruktionen für den Aufruf einer Prozedur (o<strong>der</strong> Funktionsprozedur) werden durchCall() generiert. Das Resultat einer Funktionsprozedur wird durch GetResult() andie richtige Adresse gespeichert.Für Typ-Zusicherungen und Typ-Prüfungen wird durch die Prozedur TypeTest()entsprechen<strong>der</strong> Code generiert. Ist equal = TRUE,somüssendieTypen<strong>der</strong>Variableund<strong>der</strong> Zusicherung bzw. Prüfung exakt gleich sein, ansonsten kann die Variable auch eineErweiterung des zugesicherten bzw. geprüften Typs sein. Ist guard = TRUE, sowerdenInstruktionen zur Erzeugung eines Laufzeitfehlers ausgegeben, falls die Zusicherung bzw.Prüfung negativ ausfällt. Ansonsten werden lediglich die condition codes des Prozessorsentsprechend gesetzt.Für eine CASE-Anweisung wird durch die Prozedur Case() eine Tabelle mit Sprungzielenerzeugt, die den einzelnen Labels innerhalb <strong>der</strong> Anweisung entsprechen. Durchdie Prozedur AddToSP() kann Code zur Än<strong>der</strong>ung des stack pointers generiert werden.Durch die Prozedur Test() werden die innerhalb von Boolschen Ausdrücken notwendigenTest-Instruktionen generiert.


3.4 Generierung von Objektcode 49Listing 3.13 Schnittstelle von OPC.Mod (Teil 2)PROCEDURE Test(VAR item: OPL.Item);PROCEDURE Abs(VAR item: OPL.Item);PROCEDURE Adr(VAR item: OPL.Item);PROCEDURE Cap(VAR item: OPL.Item);PROCEDURE Odd(VAR item: OPL.Item);PROCEDURE UpTo(VAR low, high, res: OPL.Item);PROCEDURE Neg(VAR item: OPL.Item);PROCEDURE Not(VAR item: OPL.Item);PROCEDURE Plus(typ: OPT.Struct; VAR source, dest: OPL.Item);PROCEDURE Minus(typ: OPT.Struct; VAR source, dest: OPL.Item);PROCEDURE Mul(typ: OPT.Struct; VAR source, dest: OPL.Item);PROCEDURE Divide(typ: OPT.Struct; VAR source, dest: OPL.Item);PROCEDURE Div(VAR source, dest: OPL.Item);PROCEDURE Mod(VAR source, dest: OPL.Item);PROCEDURE Mask(mask: LONGINT; VAR dest: OPL.Item);PROCEDURE In(VAR element, set, dest: OPL.Item);PROCEDURE LoadCC(VAR item: OPL.Item);PROCEDURE Compare(kind: SHORTINT; VAR left, right, res: OPL.Item);PROCEDURE Shift(opcode: INTEGER; VAR shift, dest: OPL.Item);PROCEDURE Trap(nr: INTEGER);PROCEDURE New(VAR designator, tag: OPL.Item);PROCEDURE SYSNew(VAR designator, size: OPL.Item);PROCEDURE SYSMove(VAR sourceAdr, destAdr, length: OPL.Item);PROCEDURE SYSGet(VAR adr, dest: OPL.Item);PROCEDURE SYSPut(VAR source, address: OPL.Item);PROCEDURE SYSGetReg(VAR dest, sourceReg: OPL.Item);PROCEDURE SYSPutReg(VAR source, destReg: OPL.Item);PROCEDURE SYSBit(VAR adr, bitnr, res: OPL.Item);END OPC.Die Prozeduren Abs(), Adr(), Cap() und Odd() erzeugen Code für die vordeklariertenProzeduren ABS(), SYSTEM.ADR(), CAP() und ODD(). Literale vom Typ SET, dieVariablen als Bereichsangaben enthalten, werden durch UpTo() erzeugt.Die Negierung von ganzzahligen Variablen sowie die Inversion von SETs werdenvon<strong>der</strong> Prozedur Neg() behandelt. Negationen in Boolschen Ausdrücken werden dagegenvon Not() behandelt.Die Prozeduren Plus(), Minus(), Mul(), Divide(), Div() und Mod() erzeugen fürAusdrücke <strong>der</strong> Form dest ? source Code, wobei ?“für einen <strong>der</strong> entsprechenden”Operatoren steht. Die Prozedur Mask() wird nur intern für die Implementierung desMOD-Operators verwendet. Die Prozedur In() behandelt Prüfungen, ob ein bestimmtesElement in einer Menge enthalten ist.Durch LoadCC() wird ein Item, das eine Bedingung darstellt, in ein Datenregistergeladen. Als Seiteneffekt setzt <strong>der</strong> Prozessor seine condition codes neu, was für einenbedingten Sprung genutzt werden kann. Die Prozedur Compare() erzeugt Code fürVergleiche in Ausdrücken.


50 Der Oberon-2 Compiler OP2Code für die vordeklarierten Prozeduren ASH(), SYSTEM.LSH() sowie SYSTEM.ROT()wird in <strong>der</strong> Prozedur Shift() generiert. Die Prozedur Trap() erzeugt einen nichtbedingtenLaufzeitfehler, <strong>der</strong> für die vordeklarierte Prozedur HALT() benötigt wird. Fürdie vordeklarierte Prozedur NEW() wird in New() <strong>der</strong> entsprechende Code erzeugt, umdie zuständige Routine des Laufzeitsystems aufzurufen.Durch die Prozeduren SYSNew(), SYSMove(), SYSGet(), SYSPut(), SYSGetReg(),SYSPutReg() und SYSBit() wird Code für die entsprechenden Prozeduren aus demPseudo-Modul SYSTEM generiert.Interne Bezeichner: Die Variablen SP und FP vom Typ Item beschreiben den Stackbzw.Frame-Pointer, <strong>der</strong> im erzeugten Code verwendet wird. In <strong>der</strong> hier beschriebenenVersion des Compilers wird das Register A7 für den Stack, das Register A6 für den Framebenutzt.3.4.4 Low-level Codegenerator (OPL.Mod)Die dritte und letzte Stufe <strong>der</strong> Codegenerierung wird in OP2 durch das Modul OPL.Modrealisiert, dessen Schnittstelle, wie in Listing 3.14 und Listing 3.15 gezeigt, aufgebautist.Intern deklariert das Modul verschiedene Arrays, in denen zum Beispiel die erzeugtenCode- und Datensegmente verwaltet werden. Ihr Inhalt wird nach <strong>der</strong> Übersetzung indie entsprechenden Teile <strong>der</strong> Objektdatei (siehe Abschnitt 3.4.5) gespeichert.Die Konstante NewLabel kennzeichnet Sprungmarken (labels), <strong>der</strong>en Adressen nochnicht endgültig festgelegt worden sind. Solche Sprungmarken treten zum Beispiel beiWHILE-Anweisungen auf, da bei <strong>der</strong> Übersetzung <strong>der</strong> Bedingung noch nicht bekannt ist,wieviel Platz die für die Anweisungen erzeugten Instruktionen benötigen. Sie werden ineiner sogenannten Fixup-Liste (fixup chain) verwaltet und, sobald die endgültige Adressebekannt ist, entsprechend angepaßt.Durch ConstSize wird die maximale Größe des internen Bereichs für Konstanten definiert.Die maximale Anzahl von Einstiegspunkten 7 in ein Modul wird durch MaxEntrybeschrieben. Die maximal mögliche Anzahl von Erweiterungen eines Recordtyps gibtdie Konstante MaxExts an. Die Konstanten BaseTypeOffs und MethodOffs beschreibeninnerhalb von Typdeskriptoren die Stellen, an denen die Tabellen <strong>der</strong> Basistypen bzw.Methoden zu finden sind.Der Typ Label stellt lediglich einen neuen Namen für den vordeklarierten TypLONGINT dar. Er dient zur besseren Unterscheidung zwischen normalen Parameternvom Typ LONGINT und solchen, die Sprungmarken beschreiben.Die Variable entry enthält eine Tabelle aller Einstiegspunkte, die im zu übersetzendenModul vorhandenen sind. In pc wird die aktuelle Position im internen Code-Arraygespeichert. Die Wurzel <strong>der</strong> aktuellen Fixup-Liste wird in link festgehalten. Die Variableentno enthält die aktuelle Anzahl von Einstiegspunkten. Die Größe <strong>der</strong> globalenVariablen eines Moduls wird in dsize gespeichert. In level wird die Schachtelungstiefe7 Unter einem Einstiegspunkt verstehen wir hier eine exportierte Prozedur, einen Typdeskriptor o<strong>der</strong>den BEGIN-Teil eines Moduls.


3.4 Generierung von Objektcode 51Listing 3.14 Schnittstelle von OPL.Mod (Teil 1)DEFINITION OPL;IMPORT OPT, SYSTEM;CONST NewLabel = 0; ConstSize = 10000; MaxEntry = 256;MaxExts = 7; BaseTypeOffs = 40; MethodOffs = -4;TYPE Label = LONGINT;(* type Item discussed seperately *)VAR entry: ARRAY MaxEntry OF LONGINT; pc: LONGINT; link: INTEGER;entno: INTEGER; dsize: LONGINT; level: SHORTINT; usedRegs: SET;PROCEDURE Init(opt: SET);PROCEDURE BegStat;PROCEDURE ConstWord(pos: INTEGER; val: LONGINT);PROCEDURE SetEntry(pos: INTEGER; val: LONGINT);PROCEDURE Trapcc(condition, trapnr: INTEGER);PROCEDURE Scale(size: LONGINT): INTEGER;PROCEDURE FindPtrs(typ: OPT.Struct; adr: LONGINT; VAR ptrTab: ARRAY OF LONGINT;VAR nofptrs: INTEGER);PROCEDURE AllocBytes(VAR s: ARRAY OF SYSTEM.BYTE; len: LONGINT; VAR adr: LONGINT);PROCEDURE AllocTypDesc(typ: OPT.Struct);PROCEDURE AllocConst(obj: OPT.Object; typ: OPT.Struct;VAR bytes: ARRAY OF SYSTEM.BYTE; len: LONGINT; VAR item: Item);PROCEDURE DefineLabel(VAR label: Label);PROCEDURE MergedLinks(l0, l1: Label): Label;PROCEDURE Jump(condition: INTEGER; VAR label: Label);PROCEDURE FJump(condition: INTEGER; VAR label: Label);PROCEDURE Bsr(VAR label: Label);PROCEDURE GetReg(): INTEGER;PROCEDURE GetAdrReg(): INTEGER;PROCEDURE GetFReg(): INTEGER;PROCEDURE FreeReg(VAR item: Item);PROCEDURE Lea(VAR source: Item; destReg: INTEGER);PROCEDURE LoadAdr(VAR item: Item);PROCEDURE LoadExternal(VAR item: Item);PROCEDURE Moveq(val: INTEGER; reg: INTEGER);PROCEDURE Move(VAR source, dest: Item);PROCEDURE Movem(dir, regList: INTEGER; VAR item: Item);PROCEDURE FMove(VAR source, dest: Item);PROCEDURE FMovecr(VAR item: Item; dr, controlReg: INTEGER);PROCEDURE FMovem(dir, regList: INTEGER; VAR item: Item);PROCEDURE Load(VAR item: Item);PROCEDURE FLoad(VAR item: Item);PROCEDURE AssertDestReg(typ: OPT.Struct; VAR source, dest: Item);<strong>der</strong> gerade zu übersetzenden Prozedur vermerkt. Schließlich gibt usedRegs über diemomentan belegten Register Auskunft.Die Prozedur Init() wird vor <strong>der</strong> Übersetzung eines Moduls aufgerufen, um bestimmteOptionen von <strong>der</strong> Benutzerschnittstelle an das Backend weiterzugeben. InBegStat() werden gewisse Vorbereitungen getroffen, die jeweils vor <strong>der</strong> Übersetzungeiner Anweisung notwendig sind.


52 Der Oberon-2 Compiler OP2Einträge in den Bereich für Konstanten können durch die Prozedur ConstWord()gemacht werden. SetEntry() erzeugt einen Eintrag in <strong>der</strong> Tabelle <strong>der</strong> Einstiegspunkte.Die Prozedur Trapcc() erzeugt Code für bedingte Ausnahmen, die zu einem Laufzeitfehlerführen; zum Beispiel prüft eine solche Anweisung, ob ein Zeiger, <strong>der</strong> <strong>der</strong>eferenziertwerden soll, den Wert NIL hat. Durch Scale() wird für bestimmte Instruktionen<strong>der</strong> richtige Skalierungsfaktor ermittelt.Mittels FindPtrs() kann für einen Typ eine Tabelle von den in ihm enthaltenenZeigern erstellt werden. Eine solche Tabelle wird zum Beispiel bei <strong>der</strong> Generierung <strong>der</strong>Typdeskriptoren benötigt.Die Prozedur AllocBytes() reserviert im Bereich für Konstanten ausreichend Platz,um die Daten, die in s übergeben werden, abzulegen. Die Adresse dieser Daten wird inadr zurückgegeben und ist auf 8 Bytes aliniert.AllocTypDesc() legt im Bereich für Konstanten für den übergebenen Typ einenTypdeskriptor an. Ein <strong>der</strong> Adresse des Deskriptors entsprechen<strong>der</strong> Einstiegspunkt wirdin typ.tdadr gespeichert.Für deklarierte Konstanten o<strong>der</strong> Literale wird ein entsprechen<strong>der</strong> Eintrag in denBereich für Konstanten mittels AllocConst() gemacht. Optional wird hier ein Itemzurückgegeben, das den Zugriff auf diese Konstante beschreibt.Sprungmarken werden mit <strong>der</strong> Prozedur DefineLabel() definiert. Neue Sprungmarkenerhalten durch sie den aktuellen Wert aus pc; sie sind damit gültig, und ihreAdresse ist festgelegt. Noch offene Sprungmarken werden durch negative Werte dargestellt,die zusammen die Fixup-Liste bilden; für sie wird hier auch die Fixup-Liste aufgelöst.Die Prozedur Jump() erzeugt einen bedingten Sprung an das übergebene Label.Falls kein gültiges Label übergeben wurde, wird die Fixup-Liste entsprechend erweitert.Für Bedingungen mit Gleitkommazahlen wird die Prozedur FJump() verwendet. DurchMergedLinks() können zwei separate Fixup-Listen zu einer neuen Liste verschmolzenwerden, was allerdings nur in komplexen Boolschen Ausdrücken erfor<strong>der</strong>lich ist. Fürden Aufruf von Prozeduren erzeugt Bsr() den entsprechenden Code.Die Prozeduren GetReg(), GetAdrReg() bzw. GetFReg() for<strong>der</strong>n das nächste freieDaten-, Adreß- bzw. Gleitkommaregister an. FreeReg() gibt die von einem Item belegtenRegister wie<strong>der</strong> frei, wobei das übergebene Item danach ungültig ist.Die Prozedur Lea() erzeugt eine load effective address“-Anweisung, mit <strong>der</strong> die”effektive Adresse des übergeben Items in ein Adreßregister geladen wird. LoadAdr()und LoadExternal() verwenden diese Prozedur, um die Adressen von internen bzw.externen Items in ein Adreßregister zu laden. Das übergebene Item wird entsprechendverän<strong>der</strong>t, so daß Zugriffe jetzt über dieses Register durchgeführt werden.Die folgenden Prozeduren Moveq(), Move(), Movem(), FMove(), FMovecr() undFMovem() erzeugen die zur Bewegung“ von Daten notwendigen MOVE-Instruktionen.”Wie zuvor werden Instruktionen für Gleitkomma-Arithmetik durch die mit F beginnendenProzeduren behandelt. Die Prozeduren Load() und FLoad() bringen ein Item inein Daten- o<strong>der</strong> Gleitkommaregister.Durch AssertDestReg() wird sichergestellt, daß ein Item in einem Register liegt. Istdies beim Aufruf noch nicht <strong>der</strong> Fall, so wird es entsprechend in ein geeignetes Registergeladen.


3.4 Generierung von Objektcode 53Listing 3.15 Schnittstelle von OPL.Mod (Teil 2)PROCEDURE TFConds(tcond: LONGINT): LONGINT;PROCEDURE TFFConds(tcond: LONGINT): LONGINT;PROCEDURE Chk(VAR item, chkItem: Item);PROCEDURE DBcc(condition: INTEGER; VAR reg: INTEGER; VAR label: Label);PROCEDURE Ext(VAR reg: Item; destSize: INTEGER);PROCEDURE Divsl(VAR source, remain<strong>der</strong>, quotient: Item);PROCEDURE Swap(VAR dest: Item);PROCEDURE Eor(VAR source, dest: Item);PROCEDURE Cmp(VAR source, dest: Item);PROCEDURE Enter(val: LONGINT);PROCEDURE Return;PROCEDURE WriteCProc(code: OPT.ConstExt);PROCEDURE Format1(opcode: LONGINT; data: INTEGER; VAR dest: Item);PROCEDURE Format6(opcode: LONGINT; data: LONGINT; VAR dest: Item);PROCEDURE Format7(opcode: LONGINT; VAR dest: Item);PROCEDURE Format2(opcode: LONGINT; VAR source, dest: Item);PROCEDURE Format3(opcode: LONGINT; VAR source: Item; destReg: INTEGER);PROCEDURE Format4(opcode: LONGINT; bitnr: LONGINT; VAR dest: Item);PROCEDURE Format5(opcode: LONGINT; VAR bitnr, dest: Item);PROCEDURE Format8(opcode: LONGINT; VAR source, dest: Item);PROCEDURE Format9(opcode: LONGINT; VAR dest: Item; offset, width: INTEGER);PROCEDURE Format10(opcode: LONGINT; offset: INTEGER; VAR width, dest: Item);PROCEDURE Format11(opcode: LONGINT; VAR source, dest: Item);PROCEDURE Format12(opcode: LONGINT; VAR source, dest: Item);PROCEDURE Format13(opcode, shiftleft: INTEGER; VAR dest: Item);PROCEDURE Format14(opcode, dr: INTEGER; VAR shift, dest: Item);PROCEDURE Format15(opcode: INTEGER; VAR item: Item);PROCEDURE OutRefPoint;PROCEDURE OutRefName(name: ARRAY OF CHAR);PROCEDURE OutRefs(obj: OPT.Object);PROCEDURE OutCode(VAR modName: ARRAY OF CHAR; key: LONGINT);PROCEDURE Close;END OPL.Die Prozeduren TFConds() und TFFConds() werden verwendet, um die durch dieM68000-Familie definierten condition codes logisch zu negieren; TFFConds() behandeltspeziell die condition codes für Gleitkomma-Arithmetik. Durch Chk() wird ein Maschinenbefehlzur Bereichsprüfung (zum Beispiel bei Zugriffen auf Arrays) erzeugt.Die Prozedur DBcc() erzeugt einen Maschinenbefehl für bedingte Schleifen. DieMaschinenbefehle für vorzeichenerhaltende Erweiterungen <strong>der</strong> unterschiedlichen Datentypen<strong>der</strong> M68000-Familie werden durch die Prozedur Ext() generiert.Mittels Divsl() wird <strong>der</strong> Code für Divisionen mit Vorzeichen generiert. Die ProzedurSwap() erzeugt Code für den Austausch zweier Registerhälften (die oberen und dieunteren 16 Bit werden gegeneinan<strong>der</strong> ausgetauscht). Eine Verknüpfung zweier Itemsmittels exklusivem O<strong>der</strong> wird durch die Prozedur Eor() generiert. Durch die ProzedurCmp() werden Maschinenbefehle für Vergleiche generiert.


54 Der Oberon-2 Compiler OP2Die Prozeduren Enter() und Return() generieren den beim Eintritt und beimVerlassen einer Prozedur nötigen Code, um Platz auf dem Stack zu schaffen. Code-Prozeduren (siehe Abschnitt 3.3.2) werden durch WriteCProc() generiert.Die Prozeduren Format1() bis Format15() erzeugen die Bitmuster für Gruppenvon Befehlen, die ein festgelegtes und regelmäßiges Format haben. Hingegen werdenFormat9() und Format10() nicht verwendet, da OP2 keine allgemeinen Bitfel<strong>der</strong> unterstützt.8Die Prozeduren OutRefPoint(), OutRefName() und OutRefs() dienen <strong>der</strong> Ausgabevon Referenzinformationen, die innerhalb des Oberon-Systems beim Auftreten einesLaufzeitfehlers für eine symbolische Anzeige des Programmzustands dienen. Referenzinformationenwerden für jede deklarierte Prozedur sowie für den BEGIN-Teil des Modulserzeugt (siehe Modul OPV.Mod, Prozedur StatSeq()). Die Referenzinformationen werdenwährend des Übersetzungsvorgangs in eine temporäre Datei geschrieben und amEnde an die Objektdatei angehängt (siehe Modul OPM.Mod, Prozedur CloseRefObj()).Die Objektdatei selbst wird durch Aufruf <strong>der</strong> Prozedur OutCode() erzeugt. IhrFormat wird in Abschnitt 3.4.5 beschrieben. Die Prozedur Close() schließlich hat in<strong>der</strong> hier beschriebenen Version von OP2 keinerlei Bedeutung; ihr Rumpf ist leer.3.4.5 Aufbau <strong>der</strong> ObjektdateienDer Aufbau <strong>der</strong> Objektdateien ist in Listing 3.16 beschrieben, wobei wir wie<strong>der</strong> dieEBNF-Notation aus Abschnitt 3.3.4 verwenden; lediglich die tags wurden hier durchihren Wert in sedezimaler Darstellung ersetzt.Listing 3.16 Aufbau von ObjektdateienObjectFile = Hea<strong>der</strong>Block EntryBlock CommandBlock PointerBlock ImportBlockLinkBlock DataBlock CodeBlock TypeBlock ReferenceBlock.Hea<strong>der</strong>Block = 0F1X versionCode:1 refBlkLen:4 refBlkPos:4 nofEntries:2 nofCommands:2nofPointers:2 nofImports:2 nofLinks:2 varSize:4 conSize:4 codeSize:4key:4 name:24.EntryBlock = 82X {entryAdr:4}.CommandBlock = 83X {name entryAdr:4}.PointerBlock = 84X {pointerOffset:4}.ImportBlock = 85X {key:4 name}.ConstBlock = 86X {byte:1}.CodeBlock = 87X {byte:1}.ReferenceBlock = 88X {ProcRef }ProcRef = 0F8X entryAdr name {LocalRef }.LocalRef = mode:1 form:1 adr:4 name.Der versionCode ist in unserem Fall immer 36X. Wir merken hier noch an, daßdieser Aufbau <strong>der</strong> Objektdateien von an<strong>der</strong>en Beschreibungen wie in [12,15,60,65] starkabweicht. Unsere Beschreibung wurde deshalb direkt dem Quelltext entnommen.8 Der vordeklarierte Typ SET wird innerhalb eines 32-Bit-Worts realisiert. Durch eine Unterstützungdieser Formate könnten diese (zur Darstellung von Mengen recht engen) Grenzen erweitert werden.Allerdings wäre dann auch eine entsprechende Än<strong>der</strong>ung an Oberon-2 selbst sinnvoll, um nicht zuvielSpeicherplatz zu verschenken.


3.5 Kritische Anmerkungen 553.5 Kritische AnmerkungenDer Compiler OP2 ist von außen betrachtet zwar ein funktionales und effizientes Produkt,wenn man die ”black box“ aber öffnet, entdeckt man viele Details, die verbessertwerden könnten.Am auffälligsten ist zunächst <strong>der</strong> eigenwillige Programmierstil, <strong>der</strong> sich zum einenin kurzen und oft nicht son<strong>der</strong>lich aussagekräftigen Bezeichnern äußert, zum an<strong>der</strong>endurch wenige und meist sehr knappe Kommentare. 9 Nun mag ein guter ProgrammierstilGeschmackssache sein, aber ein gewisses Grundmaß sollte ein solches Produkt, das alsausgesprochen portabel und damit wartbar bezeichnet wird, dennoch aufweisen.Ein weiteres Problem stellt die teilweise vorhandene ”Mißachtung“ des Modulkonzeptsdar. So werden zum Beispiel Terminalsymbole durch eindeutige Nummern gekennzeichnet.Allerdings werden die entsprechend vereinbarten Konstanten nicht aus demModul OPS.Mod exportiert und von an<strong>der</strong>en Modulen importiert, wie es das Modulkonzeptnahelegen würde. Statt dessen werden die Konstanten in jedem Modul, das ihreWerte benötigt, vollständig neu deklariert. Dies führt dazu, daß bei Än<strong>der</strong>ungen <strong>der</strong>entsprechenden Konstanten alle Module, die sie verwenden, angepaßt werden müssen.Für die Konstanten zur Beschreibung <strong>der</strong> unterschiedlichen Arten von Einträgen in <strong>der</strong>Symboltabelle gilt ähnliches. Die Prozeduren zum Import und Export von Symboltabellenim Modul OPT.Mod verwenden sogar Literale anstatt deklarierter Konstanten, wasihre Lesbarkeit nochmals vermin<strong>der</strong>t.Darüber hinaus werden auch an<strong>der</strong>e Konzepte von Oberon bzw. Oberon-2 nicht entsprechendgenutzt. Für die interne Darstellung <strong>der</strong> Symboltabelle und des Syntaxbaumshätte zum Beispiel das Konzept <strong>der</strong> Typerweiterung beson<strong>der</strong>s vorteilhaft eingesetztwerden können, um den Compiler klarer zu strukturieren: Zum einen wäre dadurch diemehrfache Nutzung bestimmter Fel<strong>der</strong> für verschiedene Daten vermeidbar gewesen, zuman<strong>der</strong>en hätten bestimmte Prüfungen durch die Verwendung des IS-Operators einfacherformuliert o<strong>der</strong> komplett durch das Typsystem übernommen werden können. 10Da OP2 ursprünglich in Oberon (und nicht in Oberon-2) entwickelt wurde, werdenin <strong>der</strong> Implementierung auch keine typgebundenen Prozeduren verwendet. Diese hätten— zusammen mit einem objektorientierten Modell für die interne Darstellung — denCompiler ebenfalls vereinfachen können: Viele <strong>der</strong> CASE-Answeisungen, die es schwermachen, den Compiler zu verstehen, wären überflüssig geworden. Außerdem hätte zumTraversieren des Syntaxbaums das Visitor-Entwurfsmuster (siehe [21, Seite 331]) verwendetwerden können, was auch die Codegenerierung vereinfacht hätte.Natürlich kann man gewisse Argumente (etwa Einfachheit <strong>der</strong> verwendeten Konzepteo<strong>der</strong> höhere Effizienz) anführen, mit denen sich auch die Architektur von OP2rechtfertigen läßt. Das allgegenwärtige ”Abwägen <strong>der</strong> Alternativen“ wird wahrscheinlichfür jeden Compiler, jeden Entwickler und jede Sprache an<strong>der</strong>s ausfallen. Deswegenist die hier formulierte Kritik auch als mein spezifischer Eindruck zu verstehen. Derinteressierte Leser möge sich anhand <strong>der</strong> Quelltexte selbst ein Bild machen.9 Dieser Programmierstil scheint an <strong>der</strong> ETH Zürich allerdings weit verbreitet zu sein. Bei Modula-Ware hat Günter Dotzel dafür die Abkürzung WCS = Wirthian condensed style geprägt, allerdingsin Zusammenhang mit [64] und dem dort vorgestellten Compiler für Oberon-0.10 Der in [26] vorgestellte Compiler für Oberon-V ist zum Beispiel so aufgebaut.


56 Der Oberon-2 Compiler OP23.6 Allgemeine Än<strong>der</strong>ungen für <strong>Fro<strong>der</strong>on</strong>Inspiriert durch die in Abschnitt 3.5 formulierte Kritik an OP2 wollen wir für den<strong>Fro<strong>der</strong>on</strong>-1-Compiler einige allgemeine Än<strong>der</strong>ungen an den Quelltexten durchführen,die nicht mit spezifischen Spracherweiterungen zusammenhängen.Zunächst benennen wir die Module des Compilers von O??.Mod auf F??.Mod umund führen entsprechende Abkürzungen in den Importlisten ein. Dies erspart uns diearbeitsintensive Än<strong>der</strong>ung aller verwendeten Modulbezeichner. Außerdem versehen wirdie Module am Anfang mit einem Kommentar, <strong>der</strong> Herkunft und Zweck <strong>der</strong> Moduleerläutert. Soweit möglich werden auch Kommentare und Anmerkungen zu vielenBezeichnern (zum Beispiel von Typen und Prozeduren), die in OP2 nicht vorhandenwaren, nachgetragen. Die entsprechenden Informationen entnehmen wir weitgehendaus [6, 10, 11].Jetzt kümmern wir uns um die zwar in vielen Modulen deklarierten, aber nicht exportiertenKonstanten, die jeweils die zulässigen Werte bestimmter Fel<strong>der</strong> in Objekten<strong>der</strong> Symboltabelle betreffen. Wir kennzeichnen sie im Modul OPT.Mod als exportiert un<strong>der</strong>setzen die Literale in den an<strong>der</strong>en Modulen durch qualifizierte Bezeichner. Zum Beispielwird die Deklaration CONST Con = 3 durch CONST Con = OPT.Con ersetzt. Auchdiesen Austausch führen wir so durch, daß <strong>der</strong> Schreibaufwand gering bleibt. Wir ersetztenalso nur die Deklaration durch einen importierten Bezeichner, nicht jede einzelneVerwendung des Bezeichners.Im Modul OPT.Mod führen wir außerdem Konstanten für die einzelnen in den Symboldateienverwendeten tags ein, was die Lesbarkeit <strong>der</strong> betroffenen Prozeduren erhöht.Im Modul OPS.Mod exportieren wir ebenfalls die Konstanten, die verschiedene Terminalsymbolevon Oberon-2 bzw. <strong>Fro<strong>der</strong>on</strong>-1 darstellen. Die entsprechenden Än<strong>der</strong>ungenwerden auch in an<strong>der</strong>en Modulen, die diese Konstanten benötigen, nachgezogen.Diese allgemeinen Än<strong>der</strong>ungen stellen natürlich nur kleine Verbesserungen dar. Wünschenswerterwäre die <strong>Entwicklung</strong> eines neuen, besser strukturierten Compilers fürOberon-2 und <strong>Fro<strong>der</strong>on</strong>-1. Auf Möglichkeiten, einen solchen Compiler zu entwickeln,gehen wir in Kapitel 7 nochmals genauer ein.


Kapitel 4Spezifikation, Axiomatische Semantikund KorrektheitBecause of its very abstractness, axiomatic semantics is oflittle direct use for some of the applications of formal languagespecifications...suchaswritingcompilersandotherlanguage systems. The applications to which it is particularyrelevant are program verification, un<strong>der</strong>standing andstandardizing languages, and, perhaps most importantly,providing help in the construction of correct programs.— Bertrand Meyer, in [44]In diesem Kapitel beschäftigen wir uns mit Beschreibungen <strong>der</strong> Semantik von Programmiersprachenund <strong>der</strong> Semantik von darin erstellten Programmen. Wir motivieren dazuzunächst die Gründe für solche Beschreibungen und führen die notwendigen Grundlagenzu ihrer Durchführung im Rahmen <strong>der</strong> axiomatischen Semantik ein. Dann versuchenwir entsprechende Spracherweiterungen für Oberon-2 zu formulieren, die eine solcheBeschreibung unterstützen. Die Grundlagen <strong>der</strong> axiomatischen Semantik werden auchin [53], [1] und [44] behandelt, wobei wir unsere Darstellung größtenteils an den erstenbeiden Quellen ausrichten.


58 Spezifikation, Axiomatische Semantik und Korrektheit4.1 Einführung und Motivation4.1.1 Korrektheit als QualitätsmerkmalInformell bezeichnen wir ein Programm als korrekt, wenn es bei gegebenen, von unsvorgesehenen Eingabedaten die von uns gewünschten Ausgabedaten in angemessenerZeit produziert. Wir sagen dann auch, daß das Programm seine Spezifikation erfülltund fehlerfrei arbeitet.Die Korrektheit eines Programms ist eines seiner entscheidenden Qualitätsmerkmale.Angesichts <strong>der</strong> möglichen Konsequenzen ist sie unserer Meinung nach sogar das bedeutendsteQualitätsmerkmal. Denn obwohl Portabilität, Wartbarkeit, Effizienz usw. natürlichauch wichtig sind, kann man die dort durch Mängel entstehenden Kosten relativleicht messen.Die Kosten für ein Programm, das nicht korrekt arbeitet, können aber unter Umständengar nicht gemessen o<strong>der</strong> beurteilt werden. 1 Das folgende, aus [25] entnommeneBeispiel illustriert dies:Ein Bestrahlungsgerät fügte Patienten schwere, in einigen Fällen tödlicheVerbrennungen zu. Wegen fehlerhafter Programmierung wurde jeweils einemehr als hun<strong>der</strong>tfach überhöhte Strahlendosis abgegeben. Die Fehlfunktiontrat nur auf, wenn <strong>der</strong> Befehlssatz von einem fingerfertigen Technikerbeson<strong>der</strong>s schnell eingetippt wurde.Konsequenzen dieser Art konfrontieren die an <strong>der</strong> <strong>Entwicklung</strong> eines Programms beteiligtenSoftware-Ingenieure mit einer Verantwortung, die — zumindest unserer Meinungnach — viel schwerer wiegt als die bezüglich an<strong>der</strong>er Qualitätsmerkmale. Die For<strong>der</strong>ungnach korrekter Software sollte deswegen auch einen angemessenen Stellenwert innerhalb<strong>der</strong> Software-<strong>Entwicklung</strong> haben.4.1.2 Testen o<strong>der</strong> Beweisen?Die Korrektheit eines Programms wird oft durch Tests überprüft: Das Programm wirdmit bestimmten Eingabedaten versorgt, und die daraus erzeugten Ausgabedaten werdenmit den von uns erwarteten Ausgabedaten verglichen. Abweichungen stellen einenHinweis auf noch vorhandene Fehler dar.Durch Tests kann aber lediglich die Präsenz von Fehlern nachgewiesen werden, nichtjedoch <strong>der</strong>en Absenz. Gerade <strong>der</strong> Nachweis, daß ein Programm keine Fehler enthält, wärewünschenswert, denn nur dann wäre sichergestellt, daß es seine Spezifikation erfüllt. Dieseschon lange bekannte Erkenntnis führte ab den 60er Jahren zur <strong>Entwicklung</strong> formalerMethoden, die Beweise für die Korrektheit eines Programms ermöglichen. Eine dieserMethoden ist die axiomatische Semantik, mit <strong>der</strong> wir uns im folgenden beschäftigenwollen. 21 Jedenfalls dann nicht, wenn wir übliche moralische und ethische Grundsätze unterstellen, dienatürlich — insbeson<strong>der</strong>e in <strong>der</strong> Industrie — nicht zwingen<strong>der</strong>weise vorhanden sein müssen.2 Es existieren auch an<strong>der</strong>e Methoden um solche Nachweise zu führen, zum Beispiel operationaleo<strong>der</strong> denotationale Semantik. In [44] werden diese Methoden genauer vorgestellt, insbeson<strong>der</strong>e wirddort auch die Äquivalenz zwischen axiomatischer und denotationaler Semantik gezeigt.


4.1 Einführung und Motivation 59Angesichts <strong>der</strong> Existenz solcher Methoden stellt sich natürlich die Frage, warum siein <strong>der</strong> Industrie nicht auf breiter Front eingesetzt werden. Der wahrscheinlichste Grundist <strong>der</strong> stets mit formalen Beweisen verbundene Aufwand, <strong>der</strong> ja auch teilweise in <strong>der</strong>Mathematik für ihre Unbeliebtheit sorgt. So ist es für einen Anfänger bestimmt mitweniger Aufwand verbunden, die Identitätn∑n(n − 1)i =1+...+ n = ,n∈ N2i=1durch Einsetzen einiger Zahlen für n zu testen, als sie durch vollständige Induktion übern zu beweisen. Um die Identität mit Sicherheit anwenden zu können, wird ihm <strong>der</strong>Beweis jedoch nicht erspart bleiben, sofern er noch nicht von jemand an<strong>der</strong>em, dem ervertraut, geführt wurde.Genau das aber ist das Problem bei <strong>der</strong> Software-<strong>Entwicklung</strong>, denn Komponenten(vergleichbar mit obiger Identität), <strong>der</strong>en Korrektheit schon formal bewiesen wurde, werdenkaum angeboten: Den Firmen, die solche Komponenten entwickeln, ist ein formalerBeweis ihrer Korrektheit scheinbar zu aufwendig. Abschließend können wir feststellen,daß die Frage Testen o<strong>der</strong> Beweisen?“ vom Standpunkt <strong>der</strong> Sicherheit her einzig und”allein mit Beweisen!“ beantwortet werden kann. 3”4.1.3 Axiomatische SemantikDie Grundlagen <strong>der</strong> axiomatischen Semantik basieren auf <strong>der</strong> mathematischen Logik,speziell auf <strong>der</strong> Prädikatenlogik: Ein Programm wird mit Zusicherungen annotiert, dieAussagen über die Werte <strong>der</strong> Variablen des Programms machen und damit dessen Zustandsraumin einer gewünschten Weise einschränken.Wenn zum Beispiel in einem Programm die Variable x: INTEGER deklariert ist, dannmacht eine Zusicherung <strong>der</strong> Form x>0 die Aussage An dieser Stelle hat die Variablex einen Wert <strong>der</strong> über 0 liegt“. Zusicherungen werden idealerweise zusammen mit”dem Programm selbst entwickelt und automatisch (statisch durch den Compiler o<strong>der</strong>dynamisch durch das Laufzeitsystem) überprüft.Um von <strong>der</strong> Gültigkeit einer Zusicherung P auf die Gültigkeit einer an<strong>der</strong>en ZusicherungQ schließen zu können, sind gewisse Axiome und Regeln erfor<strong>der</strong>lich. Diesebeschreiben die Wirkung <strong>der</strong> zwischen P und Q liegenden Anweisungen <strong>der</strong> Programmierspracheauf die in P und Q auftretenden Variablen.Durch geeignete Zusicherungen, Axiome und Regeln können dann formale Beweise<strong>der</strong> Korrektheit eines Programms geführt werden. Hierzu benötigen wir — im Prinzip— jeweils eine Zusicherung über die Eingabe- und Ausgabedaten des Programms undkönnen dann durch Anwendung <strong>der</strong> Regeln Anweisung für Anweisung“ verifizieren, ob”das Programm korrekt arbeitet. 43 Natürlich kann trotz eines Korrektheitsbeweises immer noch ein Test notwendig sein, zum Beispielum Bedingungen, die nicht formalisiert wurden (o<strong>der</strong> gar nicht formalisiert werden können), zuüberprüfen.4 Jedenfalls solange wir eine sequentielle Ausführung des Programms unterstellen. Für paralleleSysteme ist eine Verifikation ”Anweisung für Anweisung“ im allgemeinen nicht möglich.


60 Spezifikation, Axiomatische Semantik und KorrektheitZu beachten ist hier die Voraussetzung, daß wir überhaupt Aussagen über die Ausgabedatendes Programms machen können. Diese Aussagen sind nämlich nur dannrelevant, wenn das Programm auch wirklich terminiert, das heißt, wenn es nach endlicherZeit zu einem Ergebnis kommt. 5 Können wir nur unter <strong>der</strong> Annahme, daßeinProgramm auch terminiert, dessen Korrektheit beweisen, so nennen wir das Programmpartiell korrekt. Können wir dagegen zeigen, daß ein Programm wirklich terminiert, sonennen wir das Programm vollständig korrekt. Dies gilt natürlich ebenfalls nur unter<strong>der</strong> Voraussetzung, daß <strong>der</strong> von uns geführte Beweis korrekt ist.4.1.4 Vertragliches ProgrammierenIn modularen Programmiersprachen wie Oberon-2 bestehen Programme zu einem großenTeil aus Aufrufen von Prozeduren, die zum Beispiel die Funktionalität eines Betriebssystemso<strong>der</strong> einer graphischen Benutzeroberfläche nutzbar machen. Aufgrund <strong>der</strong>häufigen Verwendung solcher Prozeduren ist eine Beschreibung ihrer Semantik beson<strong>der</strong>swichtig.Eine mögliche Beschreibung besteht darin, den Aufruf einer Prozedur als Vertrag“ ”zwischen dem aufrufenden Klienten und <strong>der</strong> aufgerufenen Prozedur selbst zu verstehen:Die jeweilige Prozedur macht eine Aussage“ <strong>der</strong> Form Wenn du als Klient garantierst,” ”die Bedingung P zu erfüllen, bevor du mich aufrufst, dann garantiere ich im Gegenzug,daß nach meinem Aufruf die Bedingung Q erfüllt sein wird“. Durch einen solchenVertrag“ wird implizit (durch Angabe <strong>der</strong> Bedingungen P und Q) eineRegel im Sinne”<strong>der</strong> axiomatischen Semantik definiert. Prozeduraufrufe werden so einem formalen Beweiszugänglich.In [43] bezeichnet Meyer dieses Vorgehen als Vertragliches Programmieren (programmingby contract). Die ebenfalls in [43] vorgestellte Programmiersprache Eiffel unterstütztsolche Bedingungen als Teil <strong>der</strong> Schnittstelle einer Prozedur. Die Bedingungenwerden außerdem zur Laufzeit überprüft.Die Programmiersprache Oberon-2 bietet eine solche Möglichkeit zur Spezifikation<strong>der</strong> Semantik von Prozeduren nicht an. Es existiert zwar die vordeklarierte ProzedurASSERT(), mit <strong>der</strong>en Hilfe Boolsche Ausdrücke zur Laufzeit überprüft werden können,allerdings sind solche Prüfungen nicht aus <strong>der</strong> Schnittstelle einer Prozedur zu erkennenund damit für den Klienten nicht transparent.Zumeist behilft man sich in Oberon-2 mit entsprechenden Kommentaren, die abereine unerwünschte Redundanz einführen, da sie zusammen mit den Prüfungen innerhalb<strong>der</strong> Prozedur gewartet werden müssen. Außerdem werden solche Kommentare vonWerkzeugen, die eine textuelle Schnittstelle eines Moduls aus dessen Symboldatei erzeugen,nicht ausgegeben. Dies motiviert den Wunsch nach entsprechenden Konstrukten,um eine Spezifikation wie in Eiffel auch in Oberon-2 zu ermöglichen.5 Für Programme wie Dämonen unter UNIX, die ja normalerweise nicht terminieren, definieren wirdementsprechend die Korrektheit für jede <strong>der</strong> von ihnen ausgeführten Aufgaben.


4.2 Grundlagen <strong>der</strong> axiomatischen Semantik 614.2 Grundlagen <strong>der</strong> axiomatischen SemantikIn diesem Abschnitt stellen wir die Grundlagen <strong>der</strong> axiomatischen Semantik anhand <strong>der</strong>Semantik von Oberon-2 vor. Neben den notwendigen Beweisregeln werden wir aucheinige Beispiele behandeln, die ihre Anwendung illustrieren.4.2.1 Terminologie und NotationWir setzen im folgenden Grundkenntnisse <strong>der</strong> mathematischen Logik — und zwar sowohl<strong>der</strong> Aussagenlogik als auch <strong>der</strong> Prädikatenlogik — voraus. Eine weitergehendeEinführung in diese Gebiete ist zum Beispiel in [1] enthalten, eine mathematisch genauereDarstellung findet sich in [44]. 6Die im folgenden verwendete Notation basiert auf den in [53] und [1] eingeführten.<strong>Zur</strong> Darstellung logischer Aussagen und Verknüpfungen benutzen wir die im europäischenRaum üblichen Symbole, wie sie in Tabelle 4.1 gezeigt sind.Ausdruck Beschreibung Oberon-2a ∧ b Wahr, wenn sowohl a als auch b wahr sind. a & ba ∨ b Wahr, wenn entwe<strong>der</strong> a o<strong>der</strong> b o<strong>der</strong> beide wahr sind. a OR b¬a Wahr, wenn a falsch ist. ~aa ⇒ b Wahr, wenn b von a impliziert wird. a 42 definiert,wobei x die freie Variable ist. Setzen wir nun für x einen konkreten Wert ein (<strong>der</strong> zumBeispiel auch ”Schuh“ sein kann), dann entsteht eine Aussage, <strong>der</strong>en Wahrheitswert wirbestimmen können. Die Aussage, die dem Prädikat A(x) entspricht, ist also nur dannwahr, wenn x eine natürliche Zahl größer 42 ist.Da durch Quantoren potentiell alle möglichen Belegungen <strong>der</strong> entsprechenden Variablenrepräsentiert werden, ist folgerichtig die Aussage ∀x ∈ N : A(x) falsch, die Aussage∃x ∈ N : A(x) jedochwahr.6 Wir verweisen hier außerdem noch auf [37]. Dort wird eine ”Vorschule des vernünftigen Redens“konstruiert, die interessante Darstellungen über das Verhältnis <strong>der</strong> mathematischen Logik zur ”Wirklichkeit“enthält.


62 Spezifikation, Axiomatische Semantik und KorrektheitAnmerkung 4.1 Beweis <strong>der</strong> Stärkung und Schwächung von AussagenWir wollen die Regeln a∧b ⇒ a und a ⇒ a∨c zeigen. Dazu verwenden wir die Indentitätx ⇒ y = ¬x ∨ y sowie die üblichen Rechenregeln in Boolschen Verbänden. Es ist alsosowohlals auch(a ∧ b ⇒ a) =¬(a ∧ b) ∨ a = ¬a ∨¬b ∨ a =(¬a ∨ a) ∨¬b =wahr∨¬b =wahrwas zu beweisen war.(a ⇒ a ∨ c) =¬a ∨ (a ∨ c) =(¬a ∨ a) ∨ c =wahr∨ c =wahr,Einzelne kapitalisierte Buchstaben wie P und Q stehen im folgenden für allgemeinePrädikate, <strong>der</strong>en Anzahl an freien Variablen noch nicht festgelegt ist. Die NotationPe x bezeichnet das Prädikat P (x), in dem alle freien x durch e substituiert wurden. 7In <strong>der</strong> letzten Spalte von Tabelle 4.1 werden entsprechende Darstellungen in Oberon-2angegeben, Quantoren werden von Oberon-2 allerdings nicht unterstützt.Wir wollen außerdem noch zwei Bezeichnung einführen, die uns später hilfreich seinwerden. Eine Aussage a heißt stärker als eine Aussage b, wenna ⇒ b gilt, das heißt,wenn b durch a impliziert wird. Wir nennen b dann auch schwächer als a. Zum Beispielist die Aussage x>0stärker als die Aussage x ≥ 0, da x>0 ⇒ x ≥ 0. Allgemeinkönnen wir zeigen (siehe Anmerkung 4.1), daß sowohl a ∧ b ⇒ a als auch a ⇒ a ∨ cgilt, wir also eine Aussage a durch Konjunktionen stärken und durch Disjunktionenschwächen können.Um anzugeben, daß, wenn vor einer (eventuell zusammengesetzten) Anweisung A dasPrädikat P wahr ist, danach das Prädikat Q wahr ist, verwenden wir die Notation{P }A{Q}beziehungsweise {P }A{Q}. Innerhalb <strong>der</strong> Prädikate verwenden wir gewöhnlich die mathematischenSymbole aus Tabelle 4.1. Die Variablen innerhalb <strong>der</strong> Prädikate stehen(wenn nicht an<strong>der</strong>s angegeben) für die Variablen gleichen Namens in <strong>der</strong> Anweisung.Um die im Rahmen <strong>der</strong> axiomatischen Semantik nötigen Beweisregeln anzugeben,verwenden wir folgende Notation:H 1 ,...,H nHSie bedeutet, daß, wenn die Aussagen H 1 ,...,H n gelten, auch die Aussage H gilt. Daauch {P }A{Q} eine Aussage darstellt, darf diese Notation in solchen Beweisregeln ebenfallsverwendet werden.7 Während P (x) ja keine konkrete Aussage darstellt, ist Pe xausdrücken könnten.sehr wohl eine, die wir auch mit P (e)


4.2 Grundlagen <strong>der</strong> axiomatischen Semantik 63Bevor wir im folgenden auf einzelne Anweisungen und entsprechende Beweisregelneingehen, wollen wir zunächst noch zwei grundsätzliche, oft benötigte Beweisregeln angeben,die sich unabhängig von einer konkreten Anweisung formulieren lassen:{P }A{R}, R ⇒ Q{P }A{Q}P ⇒ R, {R}A{Q}{P }A{Q}Sie besagen, daß wir Vorbedingungen stets durch stärkere Aussagen ersetzen dürfen,während wir Nachbedingungen stets durch schwächere Aussagen ersetzen dürfen.4.2.2 Einfache Anweisungen und AnweisungsfolgenDie einfachste Anweisung in Oberon-2 ist die leere Anweisung, dieausGründen <strong>der</strong> einfacherenNotation in <strong>der</strong> Syntax auftritt. Da sie keinerlei Wirkung hat, betrifft sie auchkeine Zusicherung. Ihre Semantik läßt sich daher einfach durch {P }{P } beschreiben.Als nächstes betrachten wir die Zuweisung in Oberon-2, die in <strong>der</strong> Form x := egeschrieben wird. Der Variablen x wird durch sie <strong>der</strong> Wert des Ausdrucks e zugewiesen.Da x hierdurch verän<strong>der</strong>t wird, sind auch etwaige Zusicherungen über x von einer solchenZuweisung betroffen. Für eine Zuweisung x := e mit <strong>der</strong> Nachbedingung Q läßtsich die schwächste Vorbedingung durch P = Q x e beschreiben, es werden also alle in Qvorkommenden x durch e substituiert.{Q x e }x := e{Q}Anweisungsfolgen werden in Oberon-2 in <strong>der</strong> Form A1; A2; ...; An geschrieben.Die Semantik einer Anweisungsfolge läßt sich aus <strong>der</strong> Semantik <strong>der</strong> einzelnen Anweisungenschließen: Die Nachbedingung <strong>der</strong> Anweisung Ai-1 muß die Vorbedingung <strong>der</strong>Anweisung Ai sein o<strong>der</strong> diese implizieren. Für eine Anweisungsfolge mit n Anweisungenergibt sich also folgende Regel:Beispiel 1∀i ∈ [1,...,n]:{P i−1 }Ai{P i }{P 0 }A1; ...; An{P n }Für unser erstes Beispiel wollen wir eine Anweisungsfolge entwickeln, die die Wertezweier Variablen a, b: INTEGER austauscht. Ihre Werte seien a 0 und b 0 .


64 Spezifikation, Axiomatische Semantik und KorrektheitDamit können wir zunächst die Semantik des Austauschs an sich angeben, nicht aberseine konkrete Realisierung in Oberon-2: 8{(a = a 0 ) ∧ (b = b 0 )}{(a = b 0 ) ∧ (b = a 0 )}Wir führen nun eine Hilfsvariable t: INTEGER ein, da dies in Oberon-2 die einzigeMöglichkeit ist, den Austausch zu vollziehen. 9 Damit können wir eine vorläufige Lösungin folgen<strong>der</strong> Form angeben: 10{P 0 := (a = a 0 ) ∧ (b = b 0 )}t := a; a := b; b := t;{P 3 := (a = b 0 ) ∧ (b = a 0 )}Jetzt müssen wir zeigen, daß diese Anweisungsfolge auch wirklich die beabsichtigte Wirkunghat.Um die Regel für Anweisungsfolgen anwenden zu dürfen, müssen wir zunächst dienoch fehlenden Vor- bzw. Nachbedingungen erarbeiten. Da es sich in diesem Fall umZuweisungen handelt, besteht unser Vorgehen darin, aus einer gegebenen Nachbedingungjeweils die bezüglich einer Zuweisung schwächste Vorbedingung abzuleiten.Zunächst betrachten wir also die letzte Zuweisung und versuchen, die ZusicherungP 2 zu bestimmen:{P 2 }b := t{P 3 := (a = b 0 ) ∧ (b = a 0 )}Da a durch diese Anweisung nicht verän<strong>der</strong>t wird, muß die Bedingung a = b 0 selbst Teilvon P 2 werden. Allerdings wird b durch die Zuweisung verän<strong>der</strong>t, also ersetzen wir allefreien b durch t und erhalten schließlich P 2 := (a = b 0 ) ∧ (t = a 0 ) als Vorbedingungdieser Anweisung.Im nächsten Schritt verwenden wir die soeben abgeleitete Zusicherung P 2 als Nachbedingung<strong>der</strong> mittleren Zuweisung:{P 1 }a := b{P 2 := (a = b 0 ) ∧ (t = a 0 )}Von dieser Zuweisung ist t nicht betroffen, deshalb ist t = a 0 bestimmt Teil von P 1 .Daa verän<strong>der</strong>t wird und nach <strong>der</strong> Zuweisung a = b 0 gelten soll, muß vor <strong>der</strong> Zuweisungb = b 0 gelten. Damit können wir auch P 1 := (t = a 0 ) ∧ (b = b 0 ) bestimmen.8 Im folgenden wollen wir unter einer Anweisung“ <strong>der</strong> Form eine abstrakte Anweisung im”Sinne <strong>der</strong> strukturierten Programmierung bzw. <strong>der</strong> schrittweisen Verfeinerung verstehen.9 Von Tricks“, wie einer Verknüpfung <strong>der</strong> Werte durch ein exklusives o<strong>der</strong>“ ihrer binären Darstellung,wollen wir hier absehen.10 Die Notation P 0 := ... dient lediglich dazu, <strong>der</strong> Bedingung eine Namen zu geben. Das Symbol :=” ”ist also kein logischer Operator.


4.2 Grundlagen <strong>der</strong> axiomatischen Semantik 65Abschließend prüfen wir noch, ob sich aus <strong>der</strong> Zusicherung P 1 die Vorbedingung<strong>der</strong> ersten Zuweisung ableiten läßt, was durch Anwendung <strong>der</strong> Regel für Zuweisungenoffensichtlich wird. Damit haben wir alle Zusicherungen aufgestellt und können mit Hilfe<strong>der</strong> Regel für Anweisungsfolgen schließen, daß unsere Anweisungen genau die angestrebteWirkung haben. 114.2.3 FallunterscheidungenBei den Fallunterscheidungen wenden wir uns zunächst <strong>der</strong> IF-Anweisung zu, und zwarin drei Formen. Aufgrund <strong>der</strong> verschiedenen Möglichkeiten <strong>der</strong> IF-Anweisung erscheintes sinnvoller, verschiedene Beweisregeln anzugeben, anstatt auch für einfache Fälle dieallgemeinste Form zu verwenden.Zunächst betrachten wir die einfachste Form IF B THEN A END (B ist ein Ausdruckvom Typ BOOLEAN, A eine Anweisung), in <strong>der</strong> we<strong>der</strong> ein ELSE noch ein ELSIF vorkommt.Wenn {P }A{Q} gilt, dann muß für die gesamte Anweisung auch B gelten, sonst würdeA nicht ausgeführt und damit Q nicht erfüllt. Ist B allerdings nicht erfüllt, so müssen Pund ¬B die Nachbedingung Q implizieren. Die einfachste Regel hat also folgende Form:{P ∧ B}A{Q}, P ∧¬B ⇒ Q{P }IF B THEN A END{Q}Wenn wir nun einen ELSE-Zweig in <strong>der</strong> Form IF B THEN A1 ELSE A2 END hinzunehmen,dann müssen wir entsprechende For<strong>der</strong>ungen an beide Anweisungen und an Bverwenden. Die zweite Regel lautet also wie folgt:{P ∧ B}A1{Q}, {P ∧¬B}A2{Q}{P }IF B THEN A1 ELSE A2 END{Q}Für den allgemeinen Fall müssen wir die folgende Form <strong>der</strong> IF-Anweisung untersuchen,die sich auch durch die kursiv gesetzte Form beschreiben läßt:IF B1 THEN A1IF B1 THEN A1ELSIF B2 THEN A2ELSE IF B2 THEN A2... ...ELSIF Bn THEN AnELSE IF Bn THEN AnELSE AeELSE Ae END ENDENDENDWir könnten hier natürlich wie<strong>der</strong>holt mit <strong>der</strong> obigen Beweisregel für einfache IF-Anweisungen mit ELSE-Zweig arbeiten. Allerdings können wir auch eine neue Beweisregelangeben, <strong>der</strong>en Verwendung manchmal einfacher ist. Damit die Anweisung Ai ausgeführtwird, muß offensichtlich die Bedingung B i erfüllt sein, während die BedingungenB k , 1 ≤ k


66 Spezifikation, Axiomatische Semantik und Korrektheit{P ∧ B i ∧∀k0sgn : Z ↦→ {−1, 0, 1}, sgn(x) = 0 für x =0⎩−1 für x


4.2 Grundlagen <strong>der</strong> axiomatischen Semantik 67Während die Vorbedingung zeigt, daß je<strong>der</strong> Wert von x zu einem zulässigen Ergebnisführt, stellt die Nachbedingung eine Beschreibung <strong>der</strong> Funktion sgn(x) selbstdar.Offensichtlich müssen wir mit einer Fallunterscheidung arbeiten, um das gewünschteVerhalten in Oberon-2 zu realisieren. Wir können zum Beispiel die folgende Anweisungsfolgeverwenden:{P 1 := true}IF x < 0 THEN y := -1 ELSIF x > 0 THEN y := 1 ELSE y := 0 END{P 8 := ((x 0) ∧ (y =1))}Wie in Beispiel 1 müssen wir jetzt beweisen, daß diese Anweisungsfolge die gewünschteWirkung hat. Wir stellen zunächst fest, daß immer nur ein Zweig <strong>der</strong> IF-Anweisungzutreffen wird. Die Reihenfolge <strong>der</strong> Zweige ist also nicht relevant, und wir können jedenZweig geson<strong>der</strong>t behandeln.Die Vorbedingung <strong>der</strong> Zuweisung im ersten Zweig ist offensichtlich P 2 := true ∧ (x


68 Spezifikation, Axiomatische Semantik und Korrektheit{P ∧ B}A{P }{P }WHILE B DO A END{P ∧¬B}Da sich P von Durchlauf zu Durchlauf nicht verän<strong>der</strong>n darf, nennen wir P auch eineInvariante <strong>der</strong> Schleife.Auf ähnliche Weise können wir die Beweisregel für die REPEAT-Schleife <strong>der</strong> FormREPEAT A UNTIL B ableiten. Sei P wie<strong>der</strong> eine gegebene Vorbedingung <strong>der</strong> Schleife.Da die Bedingung <strong>der</strong> REPEAT-Schleife erst geprüft wird, wenn die Anweisung A einmalausgeführt wurde, muß diese durch {P }A{Q} beschrieben werden. Falls B noch nichterfüllt ist, wird A erneut ausgeführt. Deswegen muß zusätzlich Q∧¬B ⇒ P gelten. WennB allerdings erfüllt ist, dann gilt nach <strong>der</strong> Schleife Q∧B. Wir erhalten zusammenfassendalso folgende Regel:{P }A{Q}, Q ∧¬B ⇒ P{P }REPEAT A UNTIL B{Q ∧ B}Für Schleifen tritt hier ein bisher nicht vorhandenes Problem auf: Unsere Beweisregelnstellen nicht sicher, daß die Schleifen terminieren, daß also jemals ¬B erfülltsein wird. Um dies sicherzustellen, benötigen wir eine Funktion, die den ”Fortschritt“ inRichtung des Ziels <strong>der</strong> Schleife beschreibt. Im einfachsten Fall ist dies ein Ausdruck vomTyp INTEGER, <strong>der</strong> bei jedem Durchlauf streng monoton fällt und <strong>der</strong> beim Überschreiteneiner gewissen Schranke (zum Beispiel 0) die Bedingung ¬B impliziert. In Beispiel 3werden wir einen solchen Ausdruck angeben und damit die vollständige Korrektheit desBeispiels beweisen.Im allgemeinen ist es allerdings schwierig, einen solchen Ausdruck zu finden, etwawenn wir eine durch Zeiger verkettete lineare Liste mittels einer Schleife durchsuchenwollen. Wenn es uns nicht gelingt, einen geeigneten Ausdruck zu finden, dann könnenwir versuchen zu beweisen, daß die Schleife nicht terminiert. Hierzu müssen wir zeigen,daß nicht nur P son<strong>der</strong>n auch P ∧ B eine Invariante <strong>der</strong> Schleife ist, denn dann kann sieauch nicht terminieren.Die Behandlung von FOR-Schleifen läßt sich durch folgende Identität auf WHILE-Schleifen zurückführen: Einer Schleife <strong>der</strong> Form FOR i := beg TO end BY step DO AEND, wobeii eine ganzahlige Variable, low und high ganzzahlige Ausdrücke und stepeine ganzzahlige Konstante mit step # 0 sind, entspricht die Anweisungsfolgetemp := end; i := beg;IF step > 0 THENWHILE i = temp DO A; i := i + step ENDENDdie mit den oben vorgestellten Beweisregeln behandelt werden kann. 13 Die Variable tempwird automatisch durch den Compiler generiert und hat denselben Typ wie i. Dastep13 Im Compiler OP2 wird die FOR-Schleife sogar über diese Identität realisiert, das heißt im Syntaxbaumkommen keine Knoten für FOR mehr vor, son<strong>der</strong>n nur noch Knoten für WHILE.


4.2 Grundlagen <strong>der</strong> axiomatischen Semantik 69statisch bekannt ist, kann man in Beweisen die entsprechende Form <strong>der</strong> WHILE-Schleifewählen und die Fallunterscheidung weglassen. 14Bei <strong>der</strong> Beschreibug <strong>der</strong> LOOP-Schleife stellt sich das Problem, daß eine in ihr enthalteneEXIT-Anweisung einem GOTO an das Ende <strong>der</strong> Schleife entspricht. Solche GOTOslassen sich zwar im Rahmen <strong>der</strong> axiomatischen Semantik behandeln, allerdings müßtenwir dazu zusätzliche Mechanismen einführen. Da wir ohnehin von <strong>der</strong> Verwendung <strong>der</strong>LOOP-Schleife abraten, was, wie in [26] o<strong>der</strong> [5] gezeigt, auch zu keinerlei Problemenführt, wollen wir auf ihre axiomatische Semantik hier auch nicht weiter eingehen. 15Beispiel 3Für das dritte Beispiel wollen wir uns mit <strong>der</strong> Multiplikation zweier ganzer Zahlenbeschäftigen. Wir wollen eine Anweisungsfolge entwickeln, die das Produkt zweier nichtnegativerZahlen x, y: INTEGER berechnet und das Ergebnis an z: INTEGER zuweist.<strong>Zur</strong> Vereinfachung lassen wir einen eventuell auftretenden Überlauf außer acht. Eineerste Form unseres Algorithmus können wir wie folgt spezifizieren:{x ≥ 0 ∧ y ≥ 0}{z = xy}Eine einfache Methode, um diese Multiplikation zu realisieren, ist ihre Ersetzungdurch eine Folge von Additionen. Wir müssen also zum Beispiel x so oft auf z addieren,wie y angibt. Dies könnenwir(nachEinführung einer zusätzlichen Variablenu: INTEGER zur Vermeidung von Seiteneffekten) wie folgt erreichen:{P 1 := x ≥ 0 ∧ y ≥ 0}z := 0; u := y; WHILE u > 0 DO z := z + x; u := u - 1 END{P 6 := z = xy}Um die Korrektheit unserer Implementierung zu zeigen, wollen wir zunächst eineInvariante <strong>der</strong> Schleife bestimmen. Für unser Bespiel bietet sich hier z + ux = xyan, da wir ja so oft x zu z addieren wollen, wie u angibt. Vor <strong>der</strong> Schleife gilt damitP 2 := (z + ux = xy) ∧ (z = 0) ∧ (u = y) ∧ (u ≥ 0), woraus wir durch Einsetzenauch 0 + yx = xy überprüfen können. Nach <strong>der</strong> Schleife gilt mit dieser InvarianteP 5 := (z + ux = xy) ∧ (z = xy) ∧ (u = 0), was auch die Nachbedingung P 6 impliziert.Nun müssen noch zeigen, daß die Anweisungen im Inneren <strong>der</strong> Schleife die Invarianteerhalten. Vor <strong>der</strong> ersten Anweisung ist (wegen <strong>der</strong> Bedingung <strong>der</strong> Schleife selbst) P 3 :=(z + ux = xy) ∧ (u >0) die stärkste Aussage, die wir machen können. Nach <strong>der</strong> letztenAnweisung wurden z durch z + x und u durch u − 1 ersetzt. Durch Einsetzen könnenwir nun leicht den Erhalt <strong>der</strong> Invariante verifizieren:(z + x)+(u − 1)x = xy ⇔ z + x + ux − x = xy14 In [1] finden wir zwar eine ”echte“ Beweisregel für FOR-Schleifen in Pascal, diese kann aber aufgrund<strong>der</strong> in Oberon-2 allgemeineren Form (mit BY) nicht verwendet werden.15 Selbst Wirth gibt in [53] ihre Semantik nicht an, obwohl er die an<strong>der</strong>en Schleifen formal definiert.


70 Spezifikation, Axiomatische Semantik und Korrektheit⇔⇔z + ux +(x − x) =xyz + ux = xyDamit ist die Zusicherung nach <strong>der</strong> letzten Anweisung in <strong>der</strong> Schleife offensichtlich P 4 :=(z + ux = xy) ∧ (u ≥ 0).4.2.5 Prozeduren und FunktionsprozedurenIn diesem Kapitel beschäftigen wir uns mit <strong>der</strong> Korrektheit von Prozeduren und Funktionsprozedurenbzw. <strong>der</strong> Korrektheit entsprechen<strong>der</strong> Aufrufe.Wir wenden uns zunächst Prozeduren zu, für die keine formalen Parameter deklariertwurden, die also ausschließlich über globale Variablen mit ihrer Umgebung ”kommunizieren“.Eine solche Prozedur hat die Form PROCEDURE p; R END p, wobeip ihr Nameund R ihr Rumpf ist. Um eine Aussage über die Semantik eines Prozeduraufrufs machenzu können, benötigen wir also eine Aussage <strong>der</strong> Form {P }R{Q} über ihren Rumpf, wobeiin P und Q entsprechende, in p sichtbare Variablen enthalten können. Da die Bindungenvon Bezeichnern in R an diese Variablen schon vor dem Aufruf von p festliegen müssen,ist <strong>der</strong> Aufruf äquivalent zur Ausführung von R selbst. Die Beweisregel für Aufrufe vonProzeduren ohne formale Parameter lautet deswegen wie folgt:{P }R{Q}{P }p{Q}Bevor wir uns nun mit an<strong>der</strong>en Formen von Prozeduren beschäftigen, wollen wir hierkurz die Bedeutung <strong>der</strong> Notation Q x e in Erinnerung rufen. Sie bezeichnet ein Prädikat,in dem alle freien Variablen x durch den Ausdruck e ersetzt wurden. Es sei zum BeispielQ(x) =(0≤ x) ∧ (x ≤ a) gegeben. Dann bezeichnet Q x e das Prädikat (0 ≤ e) ∧ (e ≤ a).Prozeduren, die keine globalen Variablen verwenden, müssen notwendigerweise überihre Parameter mit ihrer Umgebung kommunizieren“. Eine solche Prozedur hat die”Form PROCEDURE p (L); R END p, wobeip wie<strong>der</strong>um für ihren Namen und R für ihrenRumpf steht. Unter L verstehen wir die Deklaration <strong>der</strong> formalen Parameter <strong>der</strong> Prozedur.Außerdem sei x eine Liste <strong>der</strong> Bezeichner dieser formalen Parameter und a eineListe entsprechen<strong>der</strong> aktueller Parameter.Wir wollen für das Folgende annehmen, daß {P }R{Q} schon bewiesen wurde. Wirmöchten dann zeigen, daß aus {P }R{Q} sowohlals auch∀x({P }p(x){Q}){P x a }p(a){Qx a }folgt. Ersteres steht für die Aussage, daß die Zusicherungen für jede Parameterliste xerfüllt sind, letzteres für die Aussage, daß eine konkrete Parameterliste a die Zusicherungenerfüllt.Allerdings können wir aus diesen Aussagen nicht ohne weiteres eine entsprechendeBeweisregel machen. Folgendes Beispiel illustriert das Problem:


4.2 Grundlagen <strong>der</strong> axiomatischen Semantik 71PROCEDURE Culprit (VAR a: INTEGER; b: INTEGER);BEGINa := b + 1;END Culprit;Mit obigen Regeln“ würden wir hier aus <strong>der</strong> offensichtlich wahren Zusicherung {true}a”:= b + 1{a = b +1} auf ∀a, b({true}Culprit(a,b){a = b +1}) schließen. Für einenkonkreten Aufruf <strong>der</strong> Form Culprit(x,x) (mit x: INTEGER) würden wir dann aberweiter auf {true}Culprit(x,x){x = x+1} schließen, was offensichtlich ein Wi<strong>der</strong>spruchist.Wir müssen also eine zusätzliche Bedingung aufstellen, um vernünftige Beweisregelnangeben zu können. Es sei (mit den obigen Bezeichnungen) x 1 <strong>der</strong> Teil von x, <strong>der</strong>inRverän<strong>der</strong>t wird, x 2 entsprechend <strong>der</strong> Teil von x, <strong>der</strong>inR nicht verän<strong>der</strong>t wird. Mit a 1und a 2 bezeichnen wir dann die entsprechenden Teile von a. Umgültige Zusicherungenableiten zu können, müssen alle Variablen in a 1 disjunkt sein und dürfen außerdem nichtin a 2 vorkommen. Wir bezeichnen diese Disjunktheitsbedingung im folgenden kurz mitdis(a).Damit können wir nun die folgenden Beweisregeln für Prozeduren, die nur auf ihreParameter zugreifen, angeben:{P }R{Q}∀x(dis(x) ⇒{P }p(x){Q})dis(a), ∀x(dis(x) ⇒{P }p(x){Q}){P x a }p(a){Qx a }Wir setzen hier aber voraus, daß lokale Variablen <strong>der</strong> Prozedur nicht Teil ihrer Nachbedingungsind. Würden wir dies nicht tun, so wären für formale Wertparameter — dieja lokale Variablen sind — falsche Schlüsse möglich.In realen Programmen kommen natürlich auch oft Prozeduren vor, die sowohl Parameterals auch globale Variablen verwenden. In diesen Fällen kann man die obenangegebenen Regeln geeignet kombinieren, um einen Korrektheitsbeweis zu führen.Abschließend wenden wir uns nun den Funktionsprozeduren zu, die allgemein in <strong>der</strong>Form PROCEDURE f (L): T; R END f geschrieben werden können. Hier bezeichnet fden Namen <strong>der</strong> Funktionsprozedur, L die formale Parameterliste, T den Typ des Resultats<strong>der</strong> Funktionsprozedur und R ihren Rumpf. Weiterhin sei x eine Liste <strong>der</strong> Bezeichner,die in L deklariert wurden.Wenn wir annehmen, daß innerhalb von R keine Zuweisungen an in L deklarierteReferenzparameter und globale Variablen erfolgen, dann ist f frei von Seiteneffekten. Derdurch einen Aufruf berechnete Wert ist demnach lediglich von den jeweiligen aktuellenParametern abhängig. Der Aufruf einer solchen Funktionsprozedur ist dann äquivalentmit <strong>der</strong> Ausführung von R, wobei die formalen Parameter durch die aktuellen Parameterersetzt wurden. Um also eine Aussage über den Aufruf <strong>der</strong> Funktionsprozedur machenzu können, benötigen wir zunächst eine Aussage <strong>der</strong> Form {P }R{Q} für ihren Rumpf.Verfügen wir über eine solche, dann können wir folgende Beweisregel für den Aufrufeiner (seiteneffektfreien) Funktionsprozedur angeben:


72 Spezifikation, Axiomatische Semantik und Korrektheit{P }R{Q}∀x(P ⇒ Q f f(x) )Damit sind nun alle wesentlich Beweisregeln für die elementaren Konstrukte <strong>der</strong> ProgrammierspracheOberon-2 definiert.4.2.6 Abstrakte DatentypenBisher haben wir uns lediglich mit mehr o<strong>der</strong> weniger ”elementaren“ Konstrukten vonProgrammiersprachen wie Oberon-2 beschäftigt. Wir können aber auch ”größere Einheiten“wie Klassen und Module im Rahmen <strong>der</strong> axiomatischen Semantik behandeln.Dazu verwenden wir sogenannte abstrakte Datentypen (ADTs).Abbildung 4.1 Formale Definition des abstrakten Datentyps StackADT Stack;TYPESElement;FUNCTIONSnew : → Stack;empty: Stack → BOOLEAN;push : Element × Stack → Stack;pop : Stack ↛ Stack;top : Stack ↛ Element;PRECONDITIONSpop (s: Stack) = (not empty(s));top (s: Stack) = (not empty(s));AXIOMSempty (new());not empty (push(x,s));top (push(x,s)) = x;pop (push(x,s)) = s;END Stack.Die formale Definition eines abstrakten Datentyps besteht im wesentlichen aus denin Abbildung 4.1 für den abstrakten Datentyp Stack gezeigten Teilen. 1616 Die hier verwendete Notation ist an die in [43] vorgestellte angelehnt. Es existieren natürlich auchan<strong>der</strong>e Notationen, siehe zum Beispiel [29].


4.2 Grundlagen <strong>der</strong> axiomatischen Semantik 73In <strong>der</strong> TYPES-Klausel listen wir weitere ADTs auf, die von unserer Definition verwendetwerden sollen. Der ADT Element selbst benötigt hier keine Definition, da wirüber ihn außer seinem Namen keine <strong>weiteren</strong> Annahmen machen. 17 Den später verwendetenADT BOOLEAN betrachten wir als vordeklariert.In <strong>der</strong> FUNCTIONS-Klausel definieren wir die Operationen, die für unseren ADTzulässig sind. Wir verwenden hierzu Funktionen im mathematischen Sinn. Da Funktionenkeine Seiteneffekte haben können, wird zum Beispiel die Operation pop als Abbildungeines Stacks auf einen an<strong>der</strong>en Stack (<strong>der</strong> nach <strong>der</strong> Anwendung <strong>der</strong> Funktion einElement weniger enthält) definiert. Funktionen, die nicht für jeden Stack definiert sind,<strong>der</strong>en Anwendung also nicht für jeden Stack erlaubt ist, kennzeichnen wir durch ↛ alspartielle Funktionen.In <strong>der</strong> PRECONDITIONS-Klausel werden für partielle Funktionen die Bedingungenangegeben, unter denen sie auf einen konkreten Stack anwendbar sind. Sie entsprechenin gewisser Weise den Vorbedingungen für Prozeduren, die wir in Abschnitt 4.2.5 besprochenhaben.In <strong>der</strong> AXIOMS-Klausel geben wir schließlich Axiome an, die sozusagen die ”Essenz“eines Stacks wie<strong>der</strong>geben. Sie beschreiben implizit den ”last-in-first-out“-Charakter, <strong>der</strong>einen Stack erst zu dem macht, was er ist. Außerdem entsprechen sie ”Rechenregeln“für den ADT, mit <strong>der</strong>en Hilfe wir komplexe Funktionsanwendungen vereinfachen undverstehen können. 18Die beiden ersten Klauseln stellen gleichsam die syntaktische Beschreibung einesADTs, die beiden letzten Klauseln seine semantische Beschreibung dar. ADTs sind vorallem deswegen interessant, weil sie lediglich das für einen Klienten sichtbare Verhaltenbeschreiben, nicht jedoch eine konkrete Implementierung (in Form eines Moduls o<strong>der</strong>einer Klasse) nahelegen. In dieser Hinsicht ist ein ADT vergleichbar mit <strong>der</strong> Spezifikationeiner Prozedur, aus <strong>der</strong>en Vor- und Nachbedingung ebenfalls nicht auf eine konkreteRealisierung geschlossen werden kann.17 Würden wir zum Beispiel einen binären Suchbaum als ADT spezifizieren, so würden wir an einElement zumindest die For<strong>der</strong>ung stellen, daß wir feststellen können, ob es kleiner als ein an<strong>der</strong>esElement ist. Die Funktion less: Element × Element → BOOLEAN“ müßte also definiert sein.” 18 Im allgemeinen ist es allerdings nicht leicht, für komplexe ADTs ein vollständiges und wi<strong>der</strong>spruchsfreiesAxiomensystem anzugeben.


74 Spezifikation, Axiomatische Semantik und Korrektheit4.2.7 Probleme für reale ProgrammiersprachenIn diesem Abschnitt gehen wir kurz auf einige Probleme im Zusammenhang mit Korrektheitsbeweisenim Rahmen <strong>der</strong> axiomatischen Semantik ein. Zum einen betreffensie die Durchführung <strong>der</strong> Korrektheitsbeweise selbst, zum an<strong>der</strong>en die Integration vonKonstrukten zu <strong>der</strong>en Unterstützung in Programmiersprachen wie Oberon-2.Beweise: Seiteneffekte, Aliasing und KomplexitätDie Verwendung <strong>der</strong> axiomatischen Semantik zum Nachweis <strong>der</strong> Korrektheit von realenProgrammen ist nicht immer so einfach, wie die oben gezeigten Beispiele vielleicht andeutenmögen. Die auftretenden Probleme lassen sich in zwei Kategorieren unterteilen:• In realen Programmiersprachen sind Konzepte vorhanden, die im Rahmen <strong>der</strong>axiomatischen Semantik nur schwer erfaßt und behandelt werden können, zumBeispiel Seiteneffekte und Aliasing von Variablen.• Die Komplexität eines Korrektheitsbeweises nimmt für reale Programme schnellAusmaße an, die für den Entwickler ohne Unterstützung durch den Computerschwer zu überschauen sind.Von Aliasing sprechen wir immer dann, wenn verschiedene Bezeichner (o<strong>der</strong> Ausdrücke)für die gleiche Variable stehen. In Abschnitt 4.2.5 wurden wir schon mit demProblem des Aliasing konfrontiert. Die dort angegebene Bedingung dis(a) für Korrektheitsbeweisevon Prozeduren dient genau dazu, die durch Aliasing entstehenden Problemezu vermeiden. Allgemeiner als bei Prozeduren treten diese Probleme bei Zeigernauf, von denen ja beliebig viele auf die gleiche anonyme Variable verweisen können.Aber auch Arrays sind in diesem Zusammenhang mit Vorsicht zu behandeln. DurchAnwendung <strong>der</strong> obigen Beweisregeln könnten wir zum Beispiel leicht{a[i] =0}a[j] := a[i] + 1{a[i] =0}beweisen. Wenn allerdings zufällig“ i = j gilt, dann ist diese Aussage offensichtlich”falsch. Zwar können Fälle, in denen Aliasing auftritt durch weitere Beweisregeln imRahmen <strong>der</strong> axiomatischen Semantik behandelt werden (siehe [44]), doch darauf könnenwir hier nicht weiter eingehen.Mit Seiteneffekten haben wir uns bisher noch nicht beschäftigt: Alle Beweisregelnwurden unter <strong>der</strong> (üblichen) Annahme formuliert, daß Ausdrücke frei von Seiteneffektensind. Durch die in Oberon-2 vorhandenen Funktionsprozeduren wird die Gültigkeitdieser Annahme aber sehr fragwürdig, denn mit ihrer Hilfe“ lassen sich ohne weiteres”auch in Ausdrücke Seiteneffekte einschleusen.


4.2 Grundlagen <strong>der</strong> axiomatischen Semantik 75Betrachten wir zum Beispiel die folgende Funktionsprozedur:PROCEDURE Side (a: INTEGER): INTEGER;BEGINx := x + a;RETURN x;END Side;Wenn wir davon ausgehen, daß x: INTEGER global zu Side() ist, dann ist die AussageSide(1) = Side(1) zum Beispiel immer falsch. Dadurch würde die Äquivalenzzwischen Ausdrücken <strong>der</strong> Programmiersprache und Ausdrücken <strong>der</strong> Zusicherungen offensichtlichverletzt, was wie<strong>der</strong>um zu fehlerhaften Beweisen führen kann. 19Schließlich sehen wir uns bei <strong>der</strong> <strong>Entwicklung</strong> eines formalen Korrektheitsbeweisesauch noch mit unserer eigenen Unfähigkeit konfrontiert:• Wir müssen eine Vielzahl von Beweis- und Rechenregeln beachten und korrektanwenden.• Wir müssen eine Vielzahl von Variablen und Beziehungen zwischen diesen überschauen.• Die korrekte Anwendung aller Regeln ist zumindest arbeitsintensiv, wenn auchnicht notwendigerweise intellektuell anspruchsvoll.Wie in Abschnitt 4.1.2 angedeutet, tragen diese Gründe ihren Teil zur geringen Beliebtheit“von formalen Korrektheitsbeweisen bei.”Glücklicherweise existieren aber Systeme, die uns bei <strong>der</strong> <strong>Entwicklung</strong> eines Korrektheitsbeweisesunterstützen. So kann zum Beispiel <strong>der</strong> Beweis-Editor“ MPE [22] gerade”die arbeitsintensiven Teile für uns durchführen (wenn auch nur unter unserer Anleitung)und uns auf eventuelle Fehler hinweisen. Bei <strong>der</strong> Verwendung eines solchen Systemsbleibt uns mehr Zeit für die interessanten Tätigkeiten wie das Auffinden von geeignetenVarianten und Invarianten. Es bleibt zu hoffen, daß Systeme wie MPE in naher Zukunftfest in <strong>Entwicklung</strong>sumgebungen wie das Oberon-System integriert sein werden.Spezifikation: Aussagen- und PrädikatenlogikDie Integration von Elementen <strong>der</strong> axiomatische Semantik in reale Programmiersprachenwirft ein weiteres Problem auf: Die axiomatische Semantik verwendet Prädikatenlogik,während die meisten realen Programmiersprachen lediglich Aussagenlogik anbieten. Insbeson<strong>der</strong>everfügen die meisten Programmiersprachen nicht über die Möglichkeit, Quantorenzu verwenden. In [43] schreibt Meyer genau zu diesem Problem Folgendes:19 Durch Seiteneffekte und Aliasing entstehen aber nicht nur Probleme für Korrektheitsbeweise. Sowohlin [26] als auch in [5] werden zum Beispiel die durch sie entstehenden Probleme für optimierendeCompiler behandelt.


76 Spezifikation, Axiomatische Semantik und KorrektheitBringing a full-fledged assertion language into Eiffel would have been theoreticallysatisfactory but totally impractical. The ability to formally expressassertions of a general nature ...requires an assertion language in whichone can directly manipulate sets, sequences, functions, relations and firstor<strong>der</strong>predicates with quantifiers (“for all” and “there exists”). Includingsuch concepts would have completely changed the nature and scope of thelanguage and made it more difficult to learn — not even mentioning the problemsof implementability and performance. Executable languages includingfull-fledged assertion sublanguages exist, but they are experimental and notsuitable for production programming. ...The Eiffel approach is a somewhatpragmatic compromise. Assertions are offered, but they are restricted toboolean expressions ...The form of assertions makes it possible to expressformally many important properties . . . but not all potentially interestingones.Da sich seit 1988 — zumindest unseres Wissens nach — nichts an dieser Situationgeän<strong>der</strong>t hat, wollen wir uns im folgenden auch im Rahmen von <strong>Fro<strong>der</strong>on</strong>-1 mit einervereinfachten Form von Zusicherungen zufrieden geben.4.3 SpracherweiterungenIn diesem Abschnitt entwickeln wir die ersten Spracherweiterungen, die <strong>Fro<strong>der</strong>on</strong>-1 vonOberon-2 unterscheiden.Sie dienen zunächst <strong>der</strong> Unterstützung von formalen Korrektheitsbeweisen. Außerdemwerden sie aber auch als Grundlage für dynamische Prüfungen verwendet, die dieEinhaltung <strong>der</strong> durch Zusicherungen gegebenen Spezifikation garantieren sollen.Wir diskutieren insbeson<strong>der</strong>e die im Rahmen des Entwurfs dieser Erweiterungenauftretenden Probleme. Auf ihre Implementierung gehen wir in Abschnitt 4.4 genauerein.4.3.1 Vor- und Nachbedingungen für ProzedurenUm Prozeduren im Sinne <strong>der</strong> axiomatischen Semantik beschreiben zu können, benötigenwir ihre Vor- und Nachbedingungen in expliziter Form. Damit wir sie innerhalb vonOberon-2 formal erfassen können, müssen wir bei <strong>der</strong> Deklaration von Prozeduren entsprechendeKlauseln für diese Bedingungen einführen. Außerdem muß <strong>der</strong> Aufbau dieserBedingungen genauer untersucht werden, was wir aber auf Abschnitt 4.3.4 verschiebenwollen.In Anlehnung an Eiffel und das von Meyer eingeführte Vertragliche Programmieren“verwenden wir die beiden Schlüsselwörter REQUIRE und ENSURE, umVor-bzw.”Nachbedingungen von Prozeduren anzugeben. Da die entsprechenden Klauseln zurSchnittstelle <strong>der</strong> Prozedur gehören und sinnvollerweise schon vor <strong>der</strong> <strong>Entwicklung</strong> <strong>der</strong>eigentlichen Prozedur angegeben werden sollten, verwenden wir folgende modifizierteSyntax für die Deklaration von Prozeduren:


4.3 Spracherweiterungen 77ProcDecl = "PROCEDURE" [Receiver] IdentDef [FormalPars] ";"ProcSpec DeclSeq ["BEGIN" StatementSeq] "END" ident.ProcSpec = ["REQUIRE" Expr ";"] ["ENSURE" Expr ";"].Hinter den neuen Schlüsselwörtern kann also jeweils ein Ausdruck stehen. Der Typdes Ausdrucks muß natürlich BOOLEAN sein, da es sich ja um Aussagen im Sinne <strong>der</strong>Aussagenlogik handelt.Wir machen diese Klauseln optional, dasiesichbei faulen“ Programmierern ansonstenoft auf ein REQUIRE TRUE bzw. ENSURE TRUE reduzieren würden, was in <strong>der</strong>”Schnittstelle mehr Verwirrung als Klarheit schaffen würde.Die Bedeutung <strong>der</strong> Zusicherungen sollte klar sein: Falls beim Aufruf <strong>der</strong> Prozedur diehinter REQUIRE angegebene Bedingung den Wert FALSE liefert, wird ein Laufzeitfehlererzeugt, ansonsten wird die Prozedur ausgeführt. Umgekehrt wird vor dem Verlassen <strong>der</strong>Prozedur die hinter ENSURE angegebene Bedingung ausgewertet. Für das Ergebnis TRUEgeht alles seinen gewohnten Gang, für FALSE wird ebenfalls ein Laufzeitfehler ausgelöst.Beispiel: Die Deklaration einer Prozedur zur Multiplikation zweier ganzer Zahlen,nach <strong>der</strong> in Abschnitt 4.2.4 Beispiel 3 gezeigten Methode könnte wie folgt aussehen:PROCEDURE Multiply (x, y: INTEGER; VAR z: INTEGER);REQUIRE (x >= 0) & (y >= 0);ENSURE z = x * y;VARu: INTEGER;BEGINz := 0; u := y;WHILE u > 0 DOz := z + x; u := u - 1ENDEND Multiply;Wie dort gezeigt, müssen wir auch hier eine zusätzliche Variable u einführen, damit wirdie Nachbedingung sinnvoll formulieren können. Wenn wir keine Zusicherungen angebenwürden, könnten wir uns diese Variable sparen und direkt y verwenden, da y innerhalb<strong>der</strong> Prozedur ja ohnehin eine lokale Variable darstellt.In Eiffel hätten wir hier die Möglichkeit auf den alten Wert von y zurückzugreifen,indem wir in <strong>der</strong> Nachbedingung die Formulierung old y verwenden. Die damit verbundenenKosten (wenn wir etwa an große Arrays denken, die als Wertparameter übergebenwerden) halten uns aber davon ab, ein solches Konstrukt auch in <strong>Fro<strong>der</strong>on</strong>-1 aufzunehmen.Operationen, die solche Kosten verursachen, sollten besser explizit durch denProgrammierer angegeben werden, als in einem kleinen Schlüsselwort“ in <strong>der</strong> Nachbedingungzu verschwinden“. 20””Die Prüfung <strong>der</strong> Nachbedingung selbst macht uns auf ein erstes Problem bei <strong>der</strong>Integration in Oberon-2 aufmerksam: In einer Prozedur kann an beliebiger Stelle die20 Dabei nehmen wir allerdings in Kauf, daß die Formulierung eines Programms mit Zusicherungenerschwert wird, da oft zusätzliche Variablen eingeführt werden müssen.


78 Spezifikation, Axiomatische Semantik und KorrektheitAnweisung RETURN stehen, die die Ausführung <strong>der</strong> Prozedur abbricht und zum Aufruferzurückkehrt. Offensichtlich müssen wir die Prüfung <strong>der</strong> Nachbedingung also sodurchführen, daß jedes vorhandene RETURN erfaßt wird.Ein weiteres Problem tritt für die Nachbedingungen von Funktionsprozeduren auf:Das Resultat einer Funktionsprozedur hat keinen expliziten Namen, wie sollen wiruns also in einer Nachbedingung darauf beziehen? Grundsätzlich haben wir hier zweiMöglichkeiten:1. Wir führen einen vordeklarierten Bezeichner mit einem festen Namen — zum BeispielRESULT — ein, <strong>der</strong> immer einen dem Resultat <strong>der</strong> Funktionsprozedur entsprechendenTyp hat. Sinnvollerweise wäre RESULT auch ein neues Schlüsselwort(ähnlich wie NIL).2. Wir verwenden den Namen <strong>der</strong> Funktionsprozedur selbst als Namen für das Resultat.Ausgehend von <strong>der</strong> Beweisregel für Funktionsprozeduren, würde sich die zweite Möglichkeitanbieten, da auch dort stellvertretend für das Ergebnis mit dem Namen <strong>der</strong>Funktionsprozedur gearbeitet wird. In Pascal wird das Resultat einer Funktionsprozedurebenfalls durch eine Zuweisung an ihren Namen bestimmt.Eine solche Konvention würde in Oberon-2 allerdings zu Verwirrungen führen (nebenbeibemerkt tut sie das auch in Pascal), denn dieser Bezeichner hätte dann abhängigvom Ort seiner Verwendung zwei völlig verschiedene Typen:• Innerhalb <strong>der</strong> Nachbedingung <strong>der</strong> Funktionsprozedur würde er den Typ ihres Ergebnisseshaben (zum Beispiel INTEGER).• Außerhalb <strong>der</strong> Nachbedingung würde er den Typ <strong>der</strong> Funktionsprozedur selbsthaben (zum Beispiel PROCEDURE (x,y: INTEGER): INTEGER).Um diese Doppeldeutigkeit zu vermeiden, entscheiden wir uns für die erste Variante undführen den vordeklarierten Bezeichner (und das Schlüsselwort) RESULT ein, <strong>der</strong> allerdingslediglich in Nachbedingungen von Funktionsprozeduren verwendet werden darf. Dorthat er den gleichen Typ wie das in <strong>der</strong> Deklaration <strong>der</strong> Funktionsprozedur angegebeneErgebnis. Er hat den Wert, <strong>der</strong> hinter <strong>der</strong> ausgeführten RETURN-Anweisung angegebenwurde.Beispiel: Die Deklaration einer Funktionsprozedur zur Multiplikation zweier ganzerZahlen könnte wie folgt aussehen:PROCEDURE Multiply (x, y: INTEGER): INTEGER;REQUIRE (x >= 0) & (y >= 0);ENSURE RESULT = x * y;VARu: INTEGER; z: INTEGER;BEGINz := 0; u := y;WHILE u > 0 DOz := z + x; u := u - 1ENDRETURN z;END Multiply;


4.3 Spracherweiterungen 79Die Erweiterung von Prozeduren durch Vor- und Nachbedingungen wirft in Oberon-2noch ein weiteres Problem auf: Wie sollen sie für Prozedurtypen und -variablen spezifiziertwerden, und wie soll bei Zuweisungen von Prozeduren an Prozedurvariablenüberprüft werden, ob die jeweiligen Bedingungen verträglich sind?Da wir Vor- und Nachbedingungen als Teil <strong>der</strong> Schnittstelle einer Prozedur ansehen,liegt es nahe, sie auch als Teil ihres Typs zu betrachten. Sei unter dieser Annahme veine Variable eines Prozedurtyps t mit den Vor- bzw. Nachbedingungen P t bzw. Q t .Sei ferner p eine Prozedur mit den Vor- bzw. Nachbedingungen P p bzw. Q p . Umnun die Zulässigkeit einer Zuweisung v := p sicherzustellen, müßten wir überprüfen,daß P t = P p und Q t = Q p gilt. 21 In [24] beschreiben John Gough und HerbertKlaeren dieses Problem wie folgt:...the question arises as to how to deal with the compatiblity of these assertions.There are several tar pits lurking here, one of them being thetheoretical incomputablity of expression equivalence, one other the praticalimpossibility of incorporating a theorem prover into a compiler . . .Es ist also nicht praktikabel, die Äquivalenz zweier allgemeiner Boolscher Ausdrückeim Rahmen eines Compilers zu überprüfen. 22Eine triviale Lösung dieses Problems wäre natürlich das Entfernen von Prozedurtypenund -variablen aus Oberon-2, wie in Abschnitt 2.7 vorgeschlagen, denn dann trittes ja gar nicht erst auf. Damit wäre <strong>Fro<strong>der</strong>on</strong>-1 allerdings nicht mehr kompatibel mitOberon-2, was wir vermeiden wollen. Einen an<strong>der</strong>en Weg (allerdings nur für Vorbedingungen)gehen Gough und Klaeren wie<strong>der</strong>um in [24]:...we resort to the KISS (“keep it simple, stupid”) principle by allowing thesaid assignments only if1. the procedure constant has no associated assertions at all (in which caseit inherits those of the type it is being assigned to) or2. the abstract syntax trees of the assertions belonging to the procedureconstant and the left hand type are identical. . .Das Problem dieser Lösung ist aber, daß sie keine einheitliche Linie für den Programmierervorgibt: Manche Zuweisungen sind erlaubt, an<strong>der</strong>e nicht, abhängig von <strong>der</strong> Implementierungdes Compilers und von <strong>der</strong> Reihenfolge in <strong>der</strong> die einzelnen Variablen ineiner Zusicherung auftreten. Für <strong>Fro<strong>der</strong>on</strong>-1 wählen wir deswegen eine ”noch einfachere“Lösung, die aber für den Programmierer klar zu durchschauen ist:• Prozedurtypen können nicht mit Zusicherungen versehen werden.• Zuweisungen von Prozeduren mit Zusicherungen an Prozedurvariablen sind verboten.21 Wie weiter unten für Methoden ausgeführt, würde es auch genügen, P t ⇒ P p bzw. Q p ⇒ Q t zuzeigen.22 Dies wird insbeson<strong>der</strong>e dann klar, wenn wir uns vor Augen halten, daß schon das einfachere ProblemP t = true NP-vollständig ist (siehe etwa [55] o<strong>der</strong> [36], Stichwort satisfiability problem, SAT).


80 Spezifikation, Axiomatische Semantik und KorrektheitEine angenehme Nebenwirkung dieser Lösung ist außerdem, daß wir die Syntax <strong>der</strong>Deklaration eines Prozedurtyps nicht verän<strong>der</strong>n müssen. Bei Vorwärtsdeklarationen vonProzeduren, für die ja das gleiche Problem auftritt, dürfen ebenfalls keine Zusicherungenangegeben werden.Nun müssen wir uns noch <strong>der</strong> Frage zuwenden, wie Vor- und Nachbedingungen imZusammenhang mit Klassen und insbeson<strong>der</strong>e im Zusammenhang mit eventuell redefiniertenMethoden gehandhabt werden sollen. Eine abgeleitete Klasse kann in Oberon-2überall dort verwendet werden, wo ihre Basisklasse erwartet wird. Das heißt, daß Klientendieser Methode lediglich die in <strong>der</strong> Basisklasse angegebenen Vorbedingungen erfüllenmüssen. Die Vorbedingung einer redefinierten Methode darf also nicht stärker sein alsdie Vorbedingung <strong>der</strong> entsprechenden Methode <strong>der</strong> Basisklasse; sie muß schwächer alsdiese sein. Umgekehrt darf die Nachbedingung einer redefinierten Methode aber auchnicht schwächer sein als die Nachbedingung <strong>der</strong> entsprechenden Methode in <strong>der</strong> Basisklasse.Auch hier verlassen sich nämlich Klienten <strong>der</strong> Methode auf die ursprünglichfür sie spezifizierte Nachbedingung (dies ist <strong>der</strong> allgemeinere Fall des in Abschnitt 6.1.5dargestellten Kovarianzproblems).Wie oben im Zusammenhang mit Prozedurtypen und -variablen ausgeführt, könnenwir nicht entscheiden, ob eine gegebene Zusicherungen stärker o<strong>der</strong> schwächer als einean<strong>der</strong>e ist. Für Methoden können wir uns aber einfacher behelfen, da wir folgendesKonstruktionsverfahren für stärkere bzw. schwächere Zusicherungen angeben können:Sei P 0 die bei <strong>der</strong> Deklaration einer redefinierten Methode M0 angegebene Vorbedingungund Q 0 ihre Nachbedingung. Seien weiterhin P 1 ,...,P n und Q 1 ,...,Q n die Vor- undNachbedingungen <strong>der</strong> von ihr überschriebenen Methoden M1, ..., Mn. Die für einenAufruf von M0 zu prüfenden Vor- und Nachbedingungen P und Q sind dann durch diefolgenden Ausdrücke gegeben:P = P n ∨ P n−1 ∨ ...∨ P 1 ∨ P 0Q = Q 0 ∧ Q 1 ∧ ...∧ Q nWie in Abschnitt 4.2.1 gezeigt, gilt dann auch∀i : P i ⇒ P∀i : Q ⇒ Q iDamit sind P und Q in geeigneter Weise stärker bzw. schwächer als die entsprechendenZusicherungen in den überschriebenen Methoden. Anzumerken ist noch, daß die vonOberon-2 benutzte Kurzschlußauswertung von Boolschen Ausdrücken hier nicht zuProblemen führt. 2323 Sie kann im Fall von Vorbedingungen sogar positive Konsequenzen haben, wenn die Verknüpfungendurch den Compiler in <strong>der</strong> oben angegeben Reihenfolge durchgeführt werden. Werden nämlich stetsdie stärksten Bedingungen zuerst geprüft, dann sind (üblicherweise) weniger Terme auszuwerten, bevordas Ergebnis feststeht. Für Nachbedingungen ist dies allerdings nicht möglich, da dort die Bedingungstets vollständig ausgewertet werden muß.


4.3 Spracherweiterungen 814.3.2 Varianten und Invarianten für Schleifen<strong>Zur</strong> Spezifikation von Schleifen müssen wir geeignete Varianten und Invarianten angeben,die ihre vollständige Korrektheit sicherstellen. Die Syntax <strong>der</strong> verschiedenen in Oberon-2vorhandenen Schleifen kann durch die folgende EBNF-Produktion beschrieben werden:Statement = [ ...| "WHILE" Expr "DO" StatementSeq "END"| "REPEAT" StatementSeq "UNTIL" Expr| "FOR" ident ":=" Expr "TO" Expr ["BY" ConstExpr]"DO" StatementSeq "END"| "LOOP" StatementSeq "END"| ...].Wie in Abschnitt 4.3.1 müssen wir auch hier geeignete neue Klauseln für unsere Zusicherungeneinführen. Allerdings entsteht durch die in Abschnitt 2.7 angesprocheneVielfalt von Schleifen das Problem, in welche Form wir diese Klauseln in die verschiedenenSchleifen integrieren sollten.Beschäftigen wir uns aber zunächst mit dem Aufbau <strong>der</strong> Klauseln selbst, <strong>der</strong> zumBeispiel durch die folgende Syntax beschrieben werden könnte:LoopSpec = ["INVARIANT" Expr] ["VARIANT" Expr].Für Invarianten muß <strong>der</strong> entsprechende Ausdruck natürlich den Typ BOOLEAN haben,wie auch schon bei den Zusicherungen für Prozeduren. Varianten hingegen stellen lautAbschnitt 4.2.4 streng monoton fallende Funktionen auf ganzen Zahlen dar. Der entsprechendeAusdruck muß also vom Typ INTEGER (o<strong>der</strong> auch SHORTINT bzw. LONGINT)sein.Die Bedeutung <strong>der</strong> Klauseln ist auch hier leicht anzugeben. Wird die angegebeneInvariante zur Laufzeit FALSE, so wird die Ausführung mit einem Laufzeitfehler abgebrochen.Dagegen wird aufgrund <strong>der</strong> Variante nur dann abgebrochen, wenn sie nacheinem Durchlauf <strong>der</strong> Schleife nicht einen um mindestens eins geringeren Wert liefert.Die Integration dieser Klauseln in die vorhandenen Schleifen könnten wir nun trivialerweisedadurch vollziehen, das wir beide zu echten Anweisungen machen. Damit wäre<strong>der</strong> Programmierer selbst für ihre korrekte Verwendung zuständig, und wir müßten nichtweiter über diesen Teil nachdenken. Allerdings ergeben sich so ähnliche Probleme wieauch bei <strong>der</strong> EXIT-Anweisung: Durch die fehlende syntaktische Bindung an die entsprechendeSchleife werden eine Reihe von zusätzlichen Prüfungen im Compiler notwendig,in unserem Fall zum Beispiel daraufhin, ob die Klauseln auch wirklich nur einmal verwendetwerden o<strong>der</strong> ob sie auch wirklich im Inneren einer Schleife verwendet werden.Bei einer syntaktische Integration entstehen diese Probleme gar nicht erst, und nichtzuletzt deswegen wollen wir diesen Weg gehen. Zunächst ist dann festzuhalten, daß auchdiese Klauseln wie<strong>der</strong> optional sein sollten. Zum einen natürlich deswegen, damit wirden Programmierer nicht dazu anhalten, sinnlose Invarianten wie INVARIANT TRUE zuformulieren. Zum an<strong>der</strong>en aber auch, da wir nicht für jeden Algorithmus ohne weiteres


82 Spezifikation, Axiomatische Semantik und Korrektheiteine sinnvolle Variante angeben können (zum Beispiel beim Traversieren einer durchZeiger verketteten linearen Liste). 24Um unsere Klauseln syntaktisch in die Schleifen einzufügen, haben wir verschiedeneMöglichkeiten. Zumindest für die ersten drei Schleifen ist es aber offensichtlich nichtsinnvoll, sie in <strong>der</strong> Nähe <strong>der</strong> Anweisungsfolge unterzubringen. In Falle <strong>der</strong> WHILE- undFOR-Schleife zum Beispiel erzeugt das Schlüsselwort DO beim Programmierer die (beabsichtigte)Assoziation, daß hier Anweisungen folgen sollen. Da Zusicherungen Teil<strong>der</strong> Spezifikation einer Schleife sind, sollten sie auch in <strong>der</strong> Nähe <strong>der</strong> (in Form einerBedingung) schon vorhandenen Spezifikation“ stehen.”Wenn wir uns unter diesen Voraussetzungen <strong>der</strong> WHILE-Schleife zuwenden, ist sicherlichdie folgende Syntax sinnvoll:Statement = [ ...| "WHILE" Expr LoopSpec "DO" StatementSeq "END"| ...].Durch sie werden unsere bisherigen For<strong>der</strong>ungen erfüllt. Lediglich die Tatsache, daß<strong>der</strong> Programmierer dadurch gezwungen wird, beide Teile <strong>der</strong> Spezifikation in einer festgelegtenReihenfolge anzugeben, scheint noch hin<strong>der</strong>lich. Allerdings wäre eine spätereAufhebung dieser strikten Reihenfolge kein Problem, wenn sie sich für Anwen<strong>der</strong> als” zu unpraktisch“ herausstellen sollte. Wir halten also vorerst im Sinne von It’s always”easier to add a feature in the next version, than to take one away“ an obiger Syntaxfest.Die entsprechende syntaktischen Än<strong>der</strong>ungen für REPEAT- und FOR-Schleifen gestaltensich dann wie folgt:Statement = [ ...| "REPEAT" StatementSeq "UNTIL" Expr LoopSpec| "FOR" ident ":=" Expr "TO" Expr ["BY" ConstExpr] LoopSpec"DO" StatementSeq "END"| ...].Bei <strong>der</strong> FOR-Schleife sollte man zunächst annehmen, daß die Angabe einer Variante nichtnötig ist. Schließlich stellt diese Schleife aufgrund ihrer Struktur eigentlich immer dieTermination sicher. Allerdings verbietet es Oberon-2 lei<strong>der</strong> nicht, daß innerhalb <strong>der</strong>Schleife Zuweisungen an die Kontrollvariable stattfinden. Es ist also durchaus möglich,eine FOR-Schleife zu formulieren, die nicht terminiert: FOR i := 0 TO 10 DO i := 2END. Da sich Fehler dieser Art auch unbemerkt einschleichen können, wollen wir sicherheitshalberauch hier die Angabe einer Invariante zulassen. 2524 In [43] gibt Meyer zwar auch für solche Probleme Varianten an, sie wirken aber sehr konstruiertbzw. künstlich. Wenn sich eine Variante aber nicht ”von selbst“ anbietet, dann sollten wir auch nichtversuchen, ”künstlich“ eine zu schaffen.25 Es wäre natürlich nicht schwer, innerhalb des Compilers eine entsprechende Prüfung durchzuführenund Zuweisungen an die Kontrollvariable (auch indirekt über Prozeduraufrufe) zu verbieten.


4.3 Spracherweiterungen 83Schließlich müssen wir uns auch noch <strong>der</strong> LOOP-Schleife zuwenden. Für sie haben wirzwar in Abschnitt 4.2.4 keine Beweisregel angegeben, allerdings sollte schon aufgrundihrer Allgemeinheit klar sein, daß auch für sie die Angabe von Invarianten und VariantenSinn machen. Mangels einer besseren Möglichkeit verwenden wir hierfür folgende Syntax:Statement = [ ...| "LOOP" LoopSpec StatementSeq "END"| ...].Damit haben wir in diesem Fall lei<strong>der</strong> kein abschließendes Schlüsselwort hinter unserenKlauseln. Allerdings würde durch ein Einfügen <strong>der</strong> Klauseln vor END ein LL(1)-Konfliktentstehen (die neue Syntax <strong>der</strong> REPEAT-Anweisung endet ja ebenfalls mit LoopSpec),was wir vermeiden wollen. Die Erweiterungen für die Spezifikation von Schleifen sinddamit abgeschlossen.4.3.3 Invarianten für Klassen und ModuleNeben Prozeduren und Schleifen macht es auch für Klassen und Module Sinn, ihren Zustandsraumeinzuschränken, um von vornherein inkonsistente Zustände auszuschließen.Daneben legt auch unsere kurze Diskussion abstrakter Datentypen in Abschnitt 4.2.6nahe, daß wir die Semantik von Klassen und Modulen genauer spezifizieren sollten.Betrachten wir zum Beispiel eine Klasse (ein Modul), die (das) den abstrakten DatentypStack (siehe Abschnitt 4.2.6) implementiert und hierzu intern ein Array verwendet.Gehen wir ferner davon aus, daß ein Überlaufen des Stacks nicht vorgesehenist, seine Größe also statisch festgelegt ist. Wenn Elements(): INTEGER eine Methode(Prozedur) ist, die die momentane Anzahl von Elementen in dem Stack angibt, undEmpty(): BOOLEAN eine Methode (Prozedur), die TRUE liefert, wenn <strong>der</strong> Stack leer ist,dann können wir die folgenden Invarianten angeben, die für jeden Stack gelten müssen:0


84 Spezifikation, Axiomatische Semantik und KorrektheitDie einzelnen Invarianten könnten wir nun durch ModSpecPart = "INVARIANT" Expr.beschreiben. Allerdings müssen wir uns für Klassen noch darum kümmern, in welcherForm die Zugehörigkeit einer Invariante zu einer Klasse ausgedrückt werden soll. Dazuhaben wir im Prinzip zwei Möglichkeiten:• Wir könnten den Mechanismus für typgebundene Prozeduren benutzen und Invariantenals spezielle Methoden behandeln. Sie müßten dann aber entwe<strong>der</strong> einenfesten Namen und eine feste Signatur haben (zum Beispiel PROCEDURE (self:Class) INVARIANT (): BOOLEAN) o<strong>der</strong> syntaktisch genauer gekennzeichnet werden(zum Beispiel in <strong>der</strong> Form INVARIANT (self: Class) Name(): BOOLEAN).• Wir könnten die schon vorhandene Klausel für die Invarianten von Modulen umeine Möglichkeit ergänzen, die die Bindung an eine Klasse ausdrückt. Hier könntenwir das in Oberon-2 schon vorhandene Konzept des Empfängerparameters fürtypgebundene Prozeduren nutzen, etwa in <strong>der</strong> Form INVARIANT (self: Class)Expression.Letztere Möglichkeit ist offensichtlich mit weniger Aufwand verbunden, da wir dann nichtgezwungen sind, in je<strong>der</strong> Klasse eine bestimme Methode zu suchen und <strong>der</strong>en Signatur zuprüfen. Allerdings würde die Syntax ModSpecPart = "INVARIANT" [Receiver] Expr.zu einem LL(1)-Konflikt führen, da ja sowohl <strong>der</strong> ”Empfängerparameter“ als auch <strong>der</strong>Ausdruck mit einer öffnenden Klammer beginnen können. Um dies zu vermeiden verwendenwir die folgende Syntax:ModSpecPart = "INVARIANT" ["FOR" ident ":" ident "IS"] Expr.Jetzt müssen wir diese Klauseln noch syntaktisch in ein Modul integrieren. Die Syntaxeines Moduls in Oberon-2 läßt sich wie folgt beschreiben:Module = "MODULE" ident ";" [ImportList] DeclSeq["BEGIN" StatementSeq] "END" ident ".".Da sich die Invarianten natürlich auf Variablen und Prozeduren des Moduls beziehen,können wir sie erst nach <strong>der</strong>en Deklaration zulassen. In die Anweisungsfolge des BEGIN-Teils sollten wir sie aber aus den obengenannten Gründen (Abschnitt 4.3.2) ebenfallsnicht einfügen. Damit bleibt eigentlich nur die folgende Möglichkeit übrig:Module = "MODULE" ident ";" [ImportList] DeclSeq ModSpec["BEGIN" StatementSeq] "END" ident ".".Aus den obengenannten Gründen ist auch die Angabe von Invarianten für Klassen undModule wie<strong>der</strong> optional.Die Semantik <strong>der</strong> Invarianten sollte klar sein: Sowohl vor als auch nach jedem Aufrufeiner Prozedur, die in diesem Modul deklariert ist, müssen die Invarianten den Wert TRUEliefern. Sollte dies nicht <strong>der</strong> Fall sein, wird das Programm wie<strong>der</strong> mit einem Laufzeitfehlerabgebrochen. Zu beachten haben wir hier, daß es aufgrund <strong>der</strong> Sichtbarkeitsregelnvon Oberon-2 auch dann nötig ist, die Invariante des Moduls zu prüfen, wenn lediglich


4.3 Spracherweiterungen 85eine Methode einer in diesem Modul deklarierten Klasse aufgerufen wird, denn auchdiese können den Zustand des Moduls verän<strong>der</strong>n.Nun ist noch die Frage zu klären, wie Module bzw. Objekte überhaupt zum erstenMal in einen konsistenten Zustand, <strong>der</strong> die Invariante erfüllt, gebracht werden.Für Module hängt dies offensichtlich mit dem Initialisierungsteil zusammen. Vor <strong>der</strong>Ausführung des Initialierungsteils wird die Invariante eines Moduls sinnvollerweise nichtgeprüft, da hier ja ein erster zulässiger Zustand des Moduls erzeugt werden soll. Nachdessen Ausführung wird die Invariante allerdings das erste Mal geprüft.Für Klassen können wir allerdings keine solche Regel angeben, da Oberon-2 nichtwie C++ o<strong>der</strong> Eiffel über Konstruktoren verfügt, die automatisch nach dem Erzeugeneines neuen Objekts ausgeführt werden und dieses in einen konsistenten Zustand bringenkönnten. Da wir uns im folgenden aber aus Zeitgründen nicht weiter mit diesem Problembeschäftigen können, müssen wir das Konzept von Invarianten für Klassen lei<strong>der</strong> vorerstaufgeben.Für die Invarianten von Modulen bleibt allerdings festzuhalten, daß sie (wie schondie Vor- und Nachbedingungen für Prozeduren) Teil <strong>der</strong> Schnittstelle des Moduls seinsollten, damit sie jedem potentiellen Klienten bekannt sind.4.3.4 Aufbau <strong>der</strong> Ausdrücke in ZusicherungenBis jetzt haben wir uns noch nicht gefragt, ob die in den neuen Klauseln auftretendenAusdrücke wirklich allgemeine Ausdrücke sein dürfen o<strong>der</strong> ob wir für sie gewisse Beschränkungeneinführen müssen. Insbeson<strong>der</strong>e das Modulkonzept von Oberon-2 sowieFunktionsprozeduren stellen uns hier vor Probleme. Im einzelnen sind die folgendenFragen zu klären:• Sollten in Zusicherungen Aufrufe von Funktionsprozeduren, die ja Seiteneffekteproduzieren können, erlaubt sein?• Sollten in Zusicherungen externe Bezeichner, also aus an<strong>der</strong>en Modulen importierteBezeichner, erlaubt sein?• Welche Sichtbarkeit sollten globale Bezeichner haben, wenn sie in Zusicherungenverwendet werden?• Sollten in Zusicherungen globale Variablen, die vollständig exportiert (also nichtschreibgeschützt exportiert) sind und damit in externen Modulen verän<strong>der</strong>t werdenkönnten, erlaubt sein?Wir wenden uns zunächst <strong>der</strong> Frage zu, ob Aufrufe von Funktionsprozeduren zulässigsind. Die zwei wichtigsten konträren Standpunkte zu dieser Frage lassen sich wie folgtzusammenfassen:• Funktionsprozeduren können Seiteneffekte verursachen, die nicht zur deklarativenNatur einer Zusicherung passen. Aufrufe von Funktionsprozeduren sollten ausdiesem Grund nicht zugelassen werden.


86 Spezifikation, Axiomatische Semantik und Korrektheit• Funktionsprozeduren können verwendet werden, um bestimmte Elemente <strong>der</strong> Prädikatenlogikin Zusicherungen zu simulieren. Aufrufe von Funktionsprozedurensollten aus diesem Grund zugelassen werden.Als Beispiel für letzteren Standpunkt betrachten wir eine Prozedur zur binären Suchein einem sortierten Array a. EinesolcheProzedurkönnte die Vorbedingung∀i, j ∈{0, 1,...,LEN(a)-1} :(i


4.4 Implementierung 87von <strong>der</strong> Verwendung externer Bezeichner, die keine Funktionsprozeduren sind, lediglichab, anstatt sie zu verbieten.Jetzt müssen wir uns mit <strong>der</strong> Frage nach <strong>der</strong> Sichtbarkeit von Bezeichnern, die innerhalbeiner Zusicherung verwendet werden, beschäftigen. Hier müssen wir unterscheiden,ob die jeweilige Zusicherung selbst Teil <strong>der</strong> Schnittstelle eines Moduls ist o<strong>der</strong> nicht.Denn damit die Schnittstelle für einen Klienten verständlich ist, sollten natürlich alleBezeichner, die in einer ebenfalls zur Schnittstelle gehörenden Zusicherung auftreten, exportiertsein. Dies betrifft in <strong>Fro<strong>der</strong>on</strong>-1 also die Invariante eines Moduls sowie die VorundNachbedingungen von exportierten Prozeduren. Für diese Zusicherungen verbietenwir also die Verwendung eines globalen Bezeichners, <strong>der</strong> selbst nicht exportiert ist. FürZusicherungen, die nicht Teil <strong>der</strong> Schnittstelle eines Moduls sind, müssen wir dagegenkeine speziellen For<strong>der</strong>ungen aufstellen. Dies betrifft in <strong>Fro<strong>der</strong>on</strong>-1 die Invarianten undVarianten von Schleifen.Schließlich ist noch zu klären, wie wir mit globalen Variablen, die vollständig (alsonicht schreibgeschützt) exportiert sind, verfahren wollen. Wenn wir an Invariantenfür Module denken, könnten wir zunächst annehmen, daß wir die Verwendung solcherVariablen verbieten müssen: Invarianten garantieren gewisse Bedingungen für Klienten,die sich auf sie verlassen. Da ein böser“ Klient sie aber je<strong>der</strong>zeit verletzen könnte,”sollten wir ihre Verwendung verbieten. Wenn wir das gleiche Problem allerdings anhandvon Vor- und Nachbedingungen von Prozeduren betrachten, so könnten wir unsauch auf einen an<strong>der</strong>en Standpunkt stellen: Die Verantwortung für die Erfüllung einerVorbedingung liegt beim Klienten des Moduls. Also hat dieser auch die Pflicht, entsprechendeglobale Variablen korrekt zu initialisieren. Dieser Standpunkt scheint nichtzuletzt deswegen besser zu sein, da wir es so vermeiden können, einen <strong>weiteren</strong> Spezialfallin die Sprache einzuführen. Trotzdem ist auch <strong>der</strong> erste Standpunkt aus einem an<strong>der</strong>enGrund weitere Überlegungen wert: Unter Umständen führt uns nach unserer momentaneDefinition nämlich ein Laufzeitfehler nicht zum eigentlichen Verursacher, son<strong>der</strong>n zueinem unschuldigen“ Klienten. Auf dieses Problem gehen wir aber in Abschnitt 4.4.3”nochmals genauer ein.4.4 ImplementierungBevor wir uns im folgenden mit <strong>der</strong> Implementierung unserer bisherigen Spracherweiterungenbeschäftigen, wollen wir noch kurz auf eine allgemeine Konvention dieser Arbeithinweisen: Um Platz zu sparen, werden die einzelnen Än<strong>der</strong>ungen am Quelltext von OP2nämlich nicht detailliert im Text dieser Arbeit beschrieben. Es werden lediglich die betroffenenModule und Prozeduren zusammen mit einer grundlegenden Beschreibung <strong>der</strong>Än<strong>der</strong>ung selbst angegeben.In Ausnahmefällen gehen wir, soweit es nötig, ist natürlich trotzdem auf ausgewählteDetails ein. Die verän<strong>der</strong>ten Quelltexte sollten aber zusammen mit dieser Arbeit verfügbargemacht worden sein. Sollten die Quelltexte aus irgendwelchen Gründen fehlen,können sie natürlich je<strong>der</strong>zeit beim Autor per email angefor<strong>der</strong>t werden (siehe Kapitel1für die entsprechende Adresse).


88 Spezifikation, Axiomatische Semantik und Korrektheit4.4.1 Allgemeine Än<strong>der</strong>ungen in Scanner und ParserBevor wir auf spezifische Än<strong>der</strong>ungen zur Implementierung <strong>der</strong> einzelnen Zusicherungeneingehen, beschäftigen wir uns in diesem Abschnitt kurz mit Än<strong>der</strong>ungen, die für jedeArt von Zusicherung nötig sind.Um überhaupt an den Erweiterungen arbeiten zu können, müssen wir zunächst dieneuen Schlüsselwörter REQUIRE, ENSURE, INVARIANT, VARIANT und RESULT in den ScannerOPS.Mod integrieren. Hierzu erweitern wir die dort schon vorhandenen ganzzahligenKonstanten um entsprechende Werte für die neuen Schlüsselwörter und tragen außerdemin <strong>der</strong> Prozedur OPS.Get() entsprechende Fälle für die CASE-Anweisung und einigeIF-Anweisungen nach.Außerdem muß natürlich <strong>der</strong> Parser OPP.Mod so modifiziert werden, daß er die neuensyntaktischen Elemente erkennt und eventuell auftretende Fehler meldet. Im einzelnenbetreffen diese Än<strong>der</strong>ungen die Prozeduren ProcedureDeclaration() für Vor- undNachbedingungen von Prozeduren, StatSeq() für Invarianten und Varianten von Schleifensowie Block() für Invarianten von Modulen. Wir fügen dort sowohl entsprechendeAnweisungen zur Erkennung und Prüfung <strong>der</strong> neuen Syntax ein als auch Aufrufe vonspeziellen, in den folgenden Abschnitten beschriebenen Prozeduren, welche die Semantik<strong>der</strong> Erweiterungen realisieren.4.4.2 Prüfung <strong>der</strong> Ausdrücke in ZusicherungenWie in Abschnitt 4.3.4 ausgeführt, müssen wir für die in Zusicherungen verwendeten Ausdrückeeine Prüfung bezüglich <strong>der</strong> Sichtbarkeit <strong>der</strong> verwendeten Bezeichner durchführen.Diese Prüfung muß den folgenden Sachverhalt prüfen und gegebenenfalls eine Fehlermeldungerzeugen:Jede Zusicherung, die Teil <strong>der</strong> Schnittstelle eines Moduls ist, darf nur solcheBezeichner enthalten, die ebenfalls in <strong>der</strong> Schnittstelle des Moduls sichtbarsind o<strong>der</strong> aus an<strong>der</strong>en Modulen importiert wurden.Um diese Prüfung zu realisieren, haben wir mehrere Möglichkeiten:• Wir können die Prüfung während des Aufbaus des Syntaxbaums für den Ausdruckim Parser OPP.Mod durchführen.• Wir können die Prüfung während des Aufbaus des Syntaxbaums für den Ausdruckim Buil<strong>der</strong> OPB.Mod durchführen.• Wir können die Prüfung auf dem für den Ausdruck erzeugten Syntaxbaum selbstdurchführen.Während die ersten beiden Lösungen Än<strong>der</strong>ungen in vielen Teilen des Compilers erfor<strong>der</strong>nwürden, ist die dritte Lösung lokal durchführbar. Da uns dies hilft, unsereÄn<strong>der</strong>ungen kompakt und übersichtlich zu halten, wählen wir die dritte Möglichkeit.Wir müssen also eine Funktionsprozedur LegalAssertExpr() entwickeln, die füreinen Ausdruck (in Form eines Syntaxbaums) überprüft, ob dieser Bezeichner enthält,


4.4 Implementierung 89die nicht in <strong>der</strong> richtigen Weise exportiert wurden. Diese Funktionsprozedur rufen wirauf, sobald <strong>der</strong> Ausdruck einer Zusicherung, die von dieser Regel betroffen ist (also VorundNachbedingungen exportierter Prozeduren sowie Invarianten von Modulen), durchden Parser verarbeitet wurde. Sie muß den für den Ausdruck erzeugten Syntaxbaumtraversieren und für den Fall, daß wir auf einen Knoten treffen, <strong>der</strong> einen in dem aktuellenModul deklarierten Bezeichner darstellt, dessen externe Sichtbarkeit überprüfen.Finden wir einen Knoten, <strong>der</strong> unsere Bedingung nicht erfüllt, müssen wir eine geeigneteFehlermeldung generieren.4.4.3 Wo sollten die Prüfung durchgeführt werden?Die von uns eingeführten Konstrukte zur Spezifikation von Oberon-2-Programmen sollenLaufzeitfehler erzeugen, wenn sie an gewissen Punkten eines Programms nicht erfülltsind. Eine bis jetzt offene Frage ist, wo diese Prüfungen durchgeführt werden sollten.Betrachten wir als Beispiel die Vor- und Nachbedingungen für Prozeduren. Offensichtlichbestehen hier grundsätzlich zwei Möglichkeiten:• Die Prüfungen erfolgen innerhalb <strong>der</strong> Prozedur, die diese Bedingungen spezifiert.• Die Prüfungen erfolgen an <strong>der</strong> Stelle des Aufrufs <strong>der</strong> Prozedur, die diese Bedingungenspezifiziert.Sowohl Prüfungen innerhalb <strong>der</strong> Prozedur, als auch Prüfungen an <strong>der</strong> Stelle des Aufrufshaben Vorteile:• Prüfungen innerhalb <strong>der</strong> Prozedur sind relativ leicht zu implementieren, indemwir am Anfang <strong>der</strong> Prozedur sowie vor je<strong>der</strong> RETURN-Anweisung eine entsprechendeIF-Anweisung generieren, die für eine nicht erfüllte Bedingung einen Laufzeitfehlererzeugt.• Prüfungen an <strong>der</strong> Stelle des Aufrufs führen uns stets zu <strong>der</strong> Stelle eines Programms,an <strong>der</strong> eine Verletzung <strong>der</strong> Zusicherungen aufgetreten ist. Etwaige Fehlerlassen sich also leichter lokalisieren. Außerdem können Prüfungen an <strong>der</strong> Stelledes Aufrufs besser optimiert werden, da dort (statisch, während <strong>der</strong> Übersetzung)mehr Informationen über die aktuellen Parameter vorhanden sind, als innerhalb<strong>der</strong> aufgerufenen Prozedur (siehe auch [24]). 27Da wir unseren Compiler ja nur einmal schreiben müssen, von besseren Fehlermeldungenund höherer Effizenz aber vielfach profitieren würden, scheint es zunächst klar, daß wirdiezweiteMöglichkeit bevorzugen sollten.Wie sollen aber bei getrennter Übersetzung die Ausdrücke <strong>der</strong> Zusicherungen für externeModule zugänglich gemacht werden? Offensichtlich wäre hierzu eine Erweiterung<strong>der</strong> Symboldatei notwendig, in <strong>der</strong> Teile des Syntaxbaums (nämlich inbeson<strong>der</strong>e dieseAusdrücke) gespeichert werden könnten. Alternativ könnte auch <strong>der</strong> gesamte Syntaxbaumähnlich wie für SDE o<strong>der</strong> Juice (siehe [16] und [14]) abgespeichert werden.27 Auf eine weitere Möglichkeit zur Optimierung gehen wir in Anmerkung 4.2 ein.


90 Spezifikation, Axiomatische Semantik und KorrektheitAnmerkung 4.2 Zusicherungen als Basis für OptimierungenWie von Gough und Klaeren in [24] gezeigt wird, können Zusicherungen — wie zumBeispiel Vor- und Nachbedingungen von Prozeduren — als Grundlage für gewisse Optimierungendienen, die ohne sie sehr viel schwerer (o<strong>der</strong> gar nicht) durchführbar wären.Betrachten wir etwa eine Prozedur, die mit einem ihr übergebenen ganzzahligen Parameteri auf ein Array zugreifen muß. Wenn für diesen Parameter eine Zusicherung <strong>der</strong>Form 0 ≤ i ≤ 10 angegeben ist und das entsprechende Array den Indexbereich 0..10hat, so kann innerhalb <strong>der</strong> Prozedur zum Beispiel auf alle Indexprüfungen verzichtetwerden. Jedenfalls dann, wenn an i keine Zuweisungen erfolgen und statisch bzw. dynamischsichergestellt wurde, daß die Bedingung beim Aufruf <strong>der</strong> Prozedur erfüllt war.Problematisch ist für uns an dieser Stelle nur, daß solche Erweiterungen in OP2nicht leicht einzubringen sind. In [24] wird für dieses Problem eine ”ad-hoc“-Lösung angegeben,die uns aber wegen ihrer fehlenden Allgemeinheit nicht zufriedenstellt. DarumsehenwirvondieserArt<strong>der</strong>Prüfung ab und wollen — wie oben angesprochen — lokalin den jeweiligen Prozeduren entsprechende IF-Anweisungen erzeugen. Allerdings solltedieser Entschluß nicht als Abwertung <strong>der</strong> Idee, Zusicherungen an <strong>der</strong> Stelle des Aufrufszu prüfen, verstanden werden. Wir wollen es lediglich vermeiden, Än<strong>der</strong>ungen von dieserTragweite, die auch die <strong>Entwicklung</strong> eines vollständig neuen Compilers rechtfertigenkönnten, in OP2 einzubringen.4.4.4 Vor- und Nachbedingungen für ProzedurenImplementierung <strong>der</strong> PrüfungenDie grundlegende Idee für die Implementierung von Vor- und Nachbedingungen vonProzeduren wurde im vorigen Abschnitt schon kurz angesprochen: Zunächst werdendie Syntaxbäume <strong>der</strong> Zusicherungen durch einen Aufruf von LegalAssertExpr() (sieheAbschnitt 4.4.2) geprüft und in lokalen Variablen gespeichert. Nachdem dann dieAnweisungsfolge <strong>der</strong> Prozedur in einen entsprechenden Syntaxbaum übersetzt wurde,fügen wir zur Prüfung <strong>der</strong> Vorbedingung eine IF-Anweisung <strong>der</strong> FormIF ~precondition THEN HALT(errorCode) END;als erste Anweisung in den Syntaxbaum ein. Ähnlich verfahren wir für die Nachbedingung:Auch für sie fügen wir entsprechende IF-Anweisungen in den Syntaxbaum ein,allerdings sowohl als letzte Anweisung als auch vor je<strong>der</strong> in <strong>der</strong> Prozedur enthaltenenRETURN-Anweisung.Die als Teil dieser Anweisung verwendete, vordeklarierte Prozedur HALT() erzeugteinen Laufzeitfehler, wobei <strong>der</strong> angegebene errorCode zur Anzeige einer passenden Fehlermeldungan das Betriebssystem weitergegeben wird. Damit erreichen wir genau dasvon uns gewünschte Verhalten: Ist eine Zusicherung nicht erfüllt, so wird die Ausführungdes Programms mit einem Laufzeitfehler abgebrochen. Um die Art des Laufzeitfehlers


4.4 Implementierung 91unterscheiden zu können, verwenden wir für Vor- bzw. Nachbedingungen verschiedeneWerte als errorCode.Im einzelnen bringen wir also die folgenden Erweiterungen in OP2 ein: Zunächstimplementieren wir die Prozedur ProcSpec(), die unsere syntaktischen Erweiterungenverarbeitet, die Ausdrücke durch einen Aufruf von LegalAssertExpr() prüft und danndie entsprechenden Syntaxbäume an zwei — zur Prozedur ProcedureDeclaration()lokale — Variablen zuweist. Nachdem die Anweisungsfolge einer Prozedur verarbeitetwurde, rufen wir die Prozedur InsertProcSpecs() auf. Durch sie werden zunächst amAnfang und am Ende <strong>der</strong> Anweisungsfolge die Prüfungen <strong>der</strong> Vor- und Nachbedingung inForm einer IF-Anweisung eingefügt. Anschließend traversiert sie den Syntaxbaum, umauch vor jedem Knoten, <strong>der</strong> eine RETURN-Anweisung repräsentiert, eine entsprechendeIF-Anweisung für die Nachbedingung zu erzeugen.Praktischerweise können wir die Erzeugung <strong>der</strong> IF-Anweisungen durch einen bereitsin OP2 vorhandenen Mechanismus erreichen: Die vordeklarierte Prozedur ASSERT()(siehe Abschnitt 4.5) wird nämlich ebenfalls in eine IF-Anweisung <strong>der</strong> exakt gleichenForm übersetzt. Wenn expr den Ausdruck unserer Zusicherung bezeichnet, können wiralso durchOPB.StPar0 (expr, assertfn);OPB.StFct (expr, assertfn, 1);die gewünschte IF-Anweisung erzeugen. Die in OP2 deklarierte Konstante assertfnstellt die interne Bezeichnung von ASSERT() dar, während die Konstante 1 diejenigeVariante <strong>der</strong> Prozedur auswählt, die lediglich einen Parameter erwartet.Implementierung von RESULTInnerhalb <strong>der</strong> Nachbedingung einer Funktionsprozedur kann — wie in Abschnitt 4.3.1beschrieben — auf das jeweilige Ergebnis (genauer: dessen Wert) durch den vordeklariertenBezeichner RESULT zugegriffen werden. Es liegt zunächst nahe, RESULT alsversteckte lokale Variable zu realisieren. Der Compiler würde diese automatisch erzeugenund ihr stets den gleichen Typ geben, den auch das Ergebnis <strong>der</strong> Funktionsprozedurselbst hat. Jede Anweisung <strong>der</strong> Form RETURN expression könnte dann auf die folgende,äquivalente Anweisungsfolge abgebildet werden:RESULT := expression;IF ~postcondition THEN HALT(errorCode) END;RETURN RESULTDamit würden Prüfungen <strong>der</strong> Nachbedingung stets mit dem richtigen Wert für das Ergebnisarbeiten.Das Problem dieser Realisierung ist jedoch, daß <strong>der</strong> Bezeichner RESULT syntaktischschon vor <strong>der</strong> Verarbeitung lokaler Variablen verwendet werden kann. Wir könnten ihnzwar auch als versteckten Teil <strong>der</strong> Parameterliste realisieren, allerdings würde dann (wieauch bei <strong>der</strong> ersten Lösung) für jede Funktionsprozedur Speicherplatz auf dem Stackverbraucht, selbst wenn RESULT gar nicht verwendet wurde.


92 Spezifikation, Axiomatische Semantik und KorrektheitUm diesen Problemen aus dem Weg zu gehen, müssen wir RESULT also als globalvordeklarierten Bezeichner (ähnlich wie NIL) behandeln, <strong>der</strong> an eine globale versteckteVariable gebunden ist. Damit können wir den notwendigen Speicherplatz statisch imRahmen des umgebenden Moduls reservieren, was im allgemeinen effizienter ist, als ihnzum Teil jedes Stackframes zu machen. 28 Die Größe dieses Speicherbereichs ist ebenfallsstatisch bekannt, da Oberon-2 die möglichen Typen für Ergebnisse von Funktionsprozedurenauf elementare Typen und Zeigertypen beschränkt. Er hat also in unserer Versionvon OP2 stets die Größe eines Langworts (4 Byte bzw. 32 Bit).Ein letztes Problem bleibt aber auch bei dieser Lösung: Da RESULT im gesamtenRumpf eines Moduls sichtbar ist, müssen wir entsprechende Vorkehrungen treffen, dieseine Verwendung auf Nachbedingungen von Funktionsprozeduren beschränken. Dieskönnen wir aber relativ leicht erreichen, da <strong>der</strong> Typ von RESULT ja ohnehin Än<strong>der</strong>ungenunterworfen ist. Innerhalb einer Funktionsprozedur hat er stets den Typ, den auch dasErgebnis dieser Funktionsprozedur hat. Hier müssen wir lediglich sicherstellen, daß ernur innerhalb einer Nachbedingung verwendet wird. Außerhalb von Funktionsprozedurensetzten wir seinen Typ einfach auf undefiniert“, wodurch seine Verwendung an”an<strong>der</strong>en Stellen unmöglich wird.Wir ergänzen also zunächst den Initialisierungsteil des Moduls OPT.Mod so, daß injedem Modul automatisch eine versteckte Variable namens @RESULT vom Typ LONGINTdeklariert wird. Anschließend müssen wir in Ausdrücken, die nicht Teil einer Nachbedingungsind, die Verwendung des Schlüsselworts RESULT verbieten. Dazu verän<strong>der</strong>n wirdie vorhandene Prozedur Factor() so, daß die Verwendung des Schlüsselworts RESULTaußerhalb einer Nachbedingung zu einer Fehlermeldung führt. Nun müssen wir eineProzedur Result() in OPB.Mod implementieren, die einen Knoten, <strong>der</strong> dem Zugriff aufRESULT entspricht, erzeugt. Außerdem muß in ProcedureDeclaration() noch <strong>der</strong> Typvon RESULT gesetzt werden. Allerdings wäre es falsch, ihn nur nach dem Erkennen desErgebnistyps <strong>der</strong> Funktionsprozedur zu setzten, da ja eventuell lokal zu dieser weitereFunktionsprozeduren deklariert sein können. Der Typ muß also zunächst gesetztwerden, um die Zusicherungen zu behandeln, dann aber erneut, nachdem alle lokalenDeklarationen verarbeitet worden sind. Schließlich muß auch noch die oben beschriebeneAbbildung von RETURN-Anweisungen durchgeführt werden, was wir in unserer schonvorhandenen Prozedur InsertProcSpecs() erledigen.Integration typgebundener ProzedurenFür typgebundene Prozeduren muß nun noch die Prozedur TProcDecl() entsprechend<strong>der</strong> obigen Beschreibung erweitert werden. Hier müssen wir allerdings zusätzlich auch anredefinierte Methoden denken: Um für sie passende Zusicherungen zu erzeugen, müssenwir das in Abschnitt 4.3.1 beschriebene Verfahren zur Konstruktion schwächerer bzw.stärkerer Vor- bzw. Nachbedingungen implementieren, was allerdings die Verfügbarkeit<strong>der</strong> Syntaxbäume aller beteiligten Zusicherungen voraussetzt.28 Da parallele Prozesse von Oberon-2 nicht unterstützt werden, kommt es für diese Speicherstelleauch nie zu Konflikten zwischen einzelnen Aufrufen.


4.4 Implementierung 93Da wir aus den in Abschnitt 4.4.3 aufgeführten Gründen jedoch keinen Zugang zu denSyntaxbäumen von Zusicherungen in externen Modulen haben, können wir dieses Verfahrenjedoch nicht realisieren. Außerdem haben wir, wie ebenfalls in Abschnitt 4.3.1beschrieben, keine Möglichkeit, um an<strong>der</strong>weitig zu prüfen, ob zwei beliebige Zusicherungeneinan<strong>der</strong>n implizieren. Die Verantwortung für die Deklaration einer ”korrektenZusicherung“ (im Sinne von stärker bzw. schwächer) für redefinierte Methoden würdedamit allerdings dem Programmierer auferlegt.Wir stehen hier also vor <strong>der</strong> Wahl, dem Programmierer diese Verantwortung ”aufzubürden“o<strong>der</strong> aufgrund dieser Unvollständigkeit den gesamten Mechanismen für VorundNachbedingungen von Prozeduren wie<strong>der</strong> zu streichen. Sicherheitshalber entscheidenwir uns hier für die zweite Variante; Vor- und Nachbedingungen werden also vorerstnicht Teil von <strong>Fro<strong>der</strong>on</strong>-1. Auf die weitreichenden Konsequenzen dieser Entscheidunggehen wir in Abschnitt 4.4.7 und Abschnitt 4.5 nochmals genauer ein.4.4.5 Varianten und Invarianten für SchleifenDie Implementierung von Invarianten für Schleifen kann nach dem in Abschnitt 4.4.4vorgestellten Verfahren erfolgen: Die Invariante wird zunächst auf eine entsprechende IF-Anweisung abbgebildet, und diese wird dann vor <strong>der</strong> ersten Anweisung des eigentlichenSchleifenrumpfs eingefügt (diese Anweisung muß auch nach <strong>der</strong> Schleife selbst nochmalseingefügt werden, siehe unten).Die Implementierung von Varianten für Schleifen erfor<strong>der</strong>t allerdings ein an<strong>der</strong>esVorgehen: Hier müssen wir uns ja bei jedem Durchlauf <strong>der</strong> Schleife vergewissern, daß<strong>der</strong> Wert <strong>der</strong> Variante — verglichen mit ihrem Wert im vorhergegangenen Durchlauf —mindestens um eins abgenommen hat. Dazu ist es offensichtlich nötig, im Inneren <strong>der</strong>Schleife eine temporäre Variable einzuführen. Der Einfachheit halber soll diese den TypLONGINT haben und mit MAX(LONGINT), alsodemgrößten möglichen Wert dieses Typs,initialisiert werden. Die Initialisierung muß natürlich vor <strong>der</strong> Schleife selbst eingefügtwerden. Wir erzeugen dann zur Prüfung <strong>der</strong> Variante eine IF-Anweisung <strong>der</strong> folgendenForm:IF (variant >= oldVariant) OR (variant < 0) THENHALT(errorCode)ELSEoldVariant := variantEND;Die Prüfung variant < 0 sorgt dafür, daß auch dann abgebrochen wird, wenn die Variantedie Schranke 0 unterschreitet. Diese Anweisung wird wie die entsprechende Anweisungfür Invarianten am Anfang des Schleifenrumpfs in den Syntaxbaum eingefügt.Die Prüfung <strong>der</strong> Invariante muß außerdem nach <strong>der</strong> Schleife nochmals durchgeführtwerden, da die unterschiedlichen Arten von Schleifen im Falle ihres Abbruchs nicht notwendigerweisenochmals zu <strong>der</strong> Stelle zurückkehren, an <strong>der</strong> wir die Prüfung in ihrenRumpf eingefügt haben. Für Varianten müssen wir die Prüfung allerdings nicht wie<strong>der</strong>holen,da sie ja ohnehin nur im Inneren <strong>der</strong> Schleife von Bedeutung sind.


94 Spezifikation, Axiomatische Semantik und KorrektheitIm einzelnen bringen wir also die folgenden Erweiterungen in OP2 ein: Zunächstlagern wir die Verarbeitung von Schleifen aus <strong>der</strong> Prozedur StatSeq() in eine neueProzedur LoopStat() aus, damit wir unsere Erweiterungen lokaler halten können. Hiererzeugen wir dann zunächst eine temporäre Variable, die für oldVariant steht. 29 Dannimplementieren wir die Prozedur LoopSpec(), die unsere syntaktischen Erweiterungenverarbeitet. Eine Prüfung <strong>der</strong> entsprechenden Ausdrücke durch LegalAssertExpr() isthier im Gegensatz zu Abschnitt 4.4.4 nicht notwendig. Die erzeugten Syntaxbäume werdenan zwei zu LoopStat() lokale Variablen zugewiesen. Nachdem die jeweilige Schleifeverarbeitet wurde, rufen wir die Prozedur InsertLoopSpecs() auf, die ähnlich wieInsertProcSpecs() aus Abschnitt 4.4.4 die erfor<strong>der</strong>lichen Än<strong>der</strong>ungen am Syntaxbaum<strong>der</strong> Schleife vornimmt: Direkt vor <strong>der</strong> Schleife fügen wir die Zuweisung oldVariant :=MAX(LONGINT) ein, während wir direkt nach ihr, wie oben beschrieben, die Prüfung <strong>der</strong>Invariante nochmals durchführen müssen. Vor die erste Anweisung des Schleifenrumpfsfügen wir anschließend die entsprechenden IF-Anweisungen zur Prüfung <strong>der</strong> Varianteund <strong>der</strong> Invariante ein.4.4.6 Invarianten für ModuleDie Implementierung von Invarianten für Module 30 kann auf die Implementierung vonVor- und Nachbedingungen für Prozeduren abgebildet werden: Die Invariante eines Modulsmuß nämlich sowohl vor als auch nach <strong>der</strong> Ausführung je<strong>der</strong> in ihm deklariertenProzedur gelten. Aus diesem Grund können wir die entsprechenden Ausdrücke einfachin die einzelnen Vor- und Nachbedingungen aufnehmen, zum Beispiel in <strong>der</strong> folgendenForm:Pneu = I ∧ P alt Qneu = I ∧ Q altDie Generierung dieser neuen“ Vor- und Nachbedingungen gestaltet sich aber schwierig,wenn wir das in Abschnitt 4.4.4 beschriebene Verfahren zur Erzeugung <strong>der</strong> IF-”Anweisungen beibehalten: Da die Deklaration <strong>der</strong> Modulinvariante erst nach <strong>der</strong> Deklarationaller Prozeduren erfolgt, müßten wir <strong>der</strong>en Syntaxbäume nachträglich verän<strong>der</strong>n.Diese nachträgliche Suche nach den entsprechenden IF-Anweisungen ist aber sowohl wenigeffizient als auch wenig elegant: Zum einen müßten wir den gesamten Syntaxbaumnochmals traversieren, zum an<strong>der</strong>en müßten wir Anweisungen richtigstellen“, die wir”besser von vornherein in <strong>der</strong> richtigen Form erzeugt hätten.Die grundlegende Idee, die uns auf ein besseres Verfahren zur Erzeugung <strong>der</strong> IF-Anweisungen für Vor- und Nachbedingungen und damit auch zu einer Implementierungfür Invarianten von Modulen führt, ist die folgende: Anstatt wie bisher die Ausdrücke<strong>der</strong> Vor- und Nachbedingungen in lokalen Variablen zu speichern (siehe Abschnitt 4.4.4),speichern wir sie in den Variablen, die während <strong>der</strong> Übersetzung zur Repräsentation29 Wir entnehmen die entsprechenden Anweisung <strong>der</strong> Implementierung von FOR, für die in OP2 jaebenfalls eine temporäre Variable generiert wird (siehe Abschnitt 4.2.4).30 Invarianten für Klassen werden aus den in Abschnitt 4.3.3 genannten Gründen nicht implementiert.Für Klassen würde aber außerdem auch das in Abschnitt 4.4.4 angesprochene Problem bezüglich desZugriffs auf die Syntaxbäume externer Zusicherungen auftreten: Auch in diesem Fall muß die Invarianteeiner abgeleiteten Klasse nämlich stets schwächer sein als die ihrer Basisklassen.


4.4 Implementierung 95einzelner Prozeduren dienen: In Variablen vom Typ ObjDesc, die Teil <strong>der</strong> Symboltabellesind (siehe Abschnitt 3.3.1). Wenn wir die Bedingung <strong>der</strong> Modulinvariante erkannthaben, können wir die Symboltabelle traversieren und den erzeugten Syntaxbaum mitden Syntaxbäumen <strong>der</strong> einzelnen Vor- und Nachbedingungen verknüpfen. Schließlichtraversieren wir dann den gesamten Syntaxbaum (aber eben nur einmal) und fügen dieentsprechenden IF-Anweisungen — die jetzt allerdings auch Modulinvarianten prüfen —wie in Abschnitt 4.4.4 dargestellt ein.Da die Modulinvariante jetzt innerhalb von Prozeduren geprüft wird, müssen wiruns noch fragen, ob durch lokale Bezeichner, die in <strong>der</strong> Modulinvariante auftretendeBezeichner verschatten, ein Problem entsteht. Dies können wir aber verneinen: Die Modulinvariantewird ja im Sichtbarkeitsbereich des Moduls selbst übersetzt. Insbeson<strong>der</strong>ewird ihr Syntaxbaum in diesem Sichtbarkeitsbereich aufgebaut. Dadurch sind in ihrauftretende Bezeichner aber auch stets an die richtigen, globalen Variablen gebunden.Selbst dadurch, daß die Auswertung des entsprechenden Ausdrucks jetzt innerhalb einerProzedur durchgeführt wird, entsteht also kein Problem.Im einzelnen müssen wir also die folgenden Än<strong>der</strong>ungen an OP2 vornehmen: ImModul OPT.Mod führen wir zur näheren Beschreibung von Prozeduren den RecordtypProcDesc als Erweiterung von ObjDesc ein. Dieser enthält die zusätzlichen Fel<strong>der</strong> preund post vom Typ Node, die jeweils den Syntaxbaum <strong>der</strong> Vor- bzw. Nachbedingungeiner Prozedur repräsentieren. Außerdem deklarieren wir eine Prozedur NewProc(), diefür Prozeduren die Aufgabe <strong>der</strong> Prozedur NewObj() übernimmt, sowie eine ProzedurInsertProc(), die als Ersatz für Insert() dient. 31 In NewProc() initialisieren wir auchdie Fel<strong>der</strong> pre und post mit einem Syntaxbaum <strong>der</strong> lediglich TRUE enthält.Das Modul OPP.Mod erweitern wir zunächst um die Prozedur ModSpec(), in<strong>der</strong>diesyntaktische Verarbeitung <strong>der</strong> Modulinvariante erfolgt. Einen entsprechenden Aufruffügen wir in Block() ein. Dieser Aufruf erfolgt aber nur dann, wenn Block() auchwirklich gerade den Deklarationsteil eines Moduls übersetzt.Dann än<strong>der</strong>n wir die Prozeduren ProcedureDeclaration() und TProcDecl() soab, daß sie unsere neuen Prozeduren OPT.NewProc() und OPT.InsertProc() verwenden.Außerdem müssen wir natürlich die durch ProcSpec() (siehe Abschnitt 4.4.4)erkannten Ausdrücke für Vor- und Nachbedingungen jetzt in dem entsprechenden Feldvon ProcDesc speichern. Weiterhin entwickeln wir die Prozedur AddModSpec(), die immerdann aufgerufen wird, wenn die Invariante eines Moduls vom Parser erkannt wurde.Sie durchsucht die Symboltabelle nach den deklarierten Prozeduren und fügt die Invariante— entsprechend unserer oben dargestellten Idee — in die schon vorhandenenSyntaxbäume für Vor- und Nachbedingungen ein.Schließlich än<strong>der</strong>n wir die Prozedur InsertProcSpec() (siehe Abschnitt 4.4.4) so ab,daß sie den gesamten Syntaxbaum traversiert und die entsprechenden IF-Anweisungenzur Prüfung <strong>der</strong> neuen Vor- und Nachbedingungen erzeugt. Ihr Aufruf muß außerdemvon ProcedureDeclaration() ans Ende von Module() verlegt werden.31 Hier bemerken wir wie<strong>der</strong> einmal, wie vorteilhaft objektorientierte Konzepte in OP2 eingesetztwerden könnten: Würden die Objekte über eine Factory wie in [21] beschrieben erzeugt, müßten wirlediglich die entsprechende Methode überschreiben. So müssen wir aber neue Prozeduren einführen,und an einigen Stellen alte gegen neue Aufrufe ersetzten.


96 Spezifikation, Axiomatische Semantik und KorrektheitAnmerkung 4.3 Eine mögliche Optimierung bei <strong>der</strong> Prüfung von ModulinvariantenFür Prozeduren (o<strong>der</strong> Methoden o<strong>der</strong> Funktionsprozeduren), die keine Seiteneffektehaben, könnten wir uns die Überprüfung <strong>der</strong> Invarianten eines Moduls nach ihrerAusführung sparen. Vor ihrer Ausführung müßten sie natürlich trotzdem geprüft werden,da sich die Prozedur ja auf die Invariante verlassen kann. Allerdings müßten wir, umin Oberon-2 festzustellen, daß eine Prozedur keine Seiteneffekte hat, den Syntaxbaumdes gesamten Moduls erneut traversieren o<strong>der</strong> während <strong>der</strong> Übersetzung entsprechendeFlags setzten. Für die in Kapitel 5 diskutierten seiteneffektfreien Funktionen wäre dieseOptimierung aber leichter durchzuführen.4.4.7 ZusammenfassungZusammenfassend können wir festhalten, daß sich die Implementierung unserer Erweiterungenzur Spezifikation von Oberon-2-Programmen im Rahmen von OP2 deutlichschwieriger gestaltet als ursprünglich angenommen. Im Bezug auf <strong>Fro<strong>der</strong>on</strong>-1 geltensie deswegen als vorerst nicht implementiert und werden deswegen auch nicht in dieSprachdefinition selbst aufgenommen.Dies sollte allerdings nicht als eine Abwertung <strong>der</strong> in diesem Kapitel vorgestelltenKonzepte betrachtet werden. Wir erachten sie nach wie vor für gut und streben prinzipiellauch ihre vollständige Implementierung an. Im Rahmen dieser Arbeit könnenwir eine solche Implementierung jedoch nicht durchführen. In <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> sollallerdings weiter an ihrer Implementierung gearbeitet werden, sowohl in bezug auf diebisher in Oberon-2 fehlenden Konstruktoren für Klassen, als auch in bezug auf einenneuen Compiler, <strong>der</strong> für einen flexibleren Umgang mit Syntaxbäumen vorbereitet ist.Alle durchführbaren Än<strong>der</strong>ungen (zum Beispiel für die Spezifikation von Schleifen) sindaber trotzdem in den Quelltexten des <strong>Fro<strong>der</strong>on</strong>-1-Compilers angedeutet.4.5 AlternativeninOberon-2Da wir die in diesem Kapitel erarbeiteten Erweiterung aus den in Abschnitt 4.4 angegebenenGründen nicht realisiert haben, wollen wir uns im folgenden damit beschäftigen,wie wir in Oberon-2 trotzdem eine Teilmenge <strong>der</strong> in Abschnitt 4.2 und Abschnitt 4.3vorgestellten Konzepte anwenden können.Als Grundlage hierfür dient die vordeklarierte Prozedur ASSERT(). Unsere Darstellungihrer Verwendung basiert auf Empfehlungen, die von Stefan Ludwig an <strong>der</strong> ETHZürich ausgearbeitet wurden und die unter an<strong>der</strong>em bei [40] verfügbar sind.


4.5 Alternativen in Oberon-2 974.5.1 Die vordeklarierte Prozedur ASSERT()Die vordeklarierte Prozedur ASSERT() kann zur Prüfung von Boolschen Ausdrückenim Sinne von Zusicherungen verwendet werden. Sie ist polymorph, da sie mit zweiverschiedenen Signaturen aufgerufen werden kann:ASSERT (expression)ASSERT (expression, integer)Beiden Formen gemein ist, daß ein Laufzeitfehler erzeugt wird, wenn <strong>der</strong> Ausdruckexpression den Wert FALSE liefert. In <strong>der</strong> zweiten Form kann <strong>der</strong> an das Betriebssystemgemeldete Wert für den Laufzeitfehler explizit angegeben werden, zum Beispiel umverschiedene Arten von Fehlern unterscheiden zu können. 324.5.2 Vor- und Nachbedingungen für ProzedurenUm Prozeduren (und Funktionsprozeduren) durch Vor- und Nachbedingungen zu spezifizieren,sollten sowohl nach dem Eintritt in die Prozedur als auch vor ihrem Verlassen(insbeson<strong>der</strong>e vor je<strong>der</strong> in ihr enthaltenen RETURN-Anweisung) entsprechende Aufrufevon ASSERT() mit geeigneten Bedingungen eingefügt werden. Außerdem sollten dieseBedingungen natürlich noch in einem entsprechenden Kommentar beschrieben werden,zum Beispiel unter Benutztung <strong>der</strong> von uns für <strong>Fro<strong>der</strong>on</strong>-1 vorgeschlagenen Syntax. 33Beispiel: Die Funktionsprozedur Multiply aus Abschnitt 4.3.1 sollte in Oberon-2wie folgt formuliert werden:PROCEDURE Multiply* (x, y: INTEGER): INTEGER;(**REQUIRE (x >= 0) & (y >= 0);ENSURE RESULT = x * y;*)VARu: INTEGER; z: INTEGER;BEGINASSERT ((x >= 0) & (y >= 0), 100); (* Vorbedingung *)z := 0; u := y;WHILE u > 0 DOz := z + x; u := u - 1ENDASSERT (z = x * y, 120); (* Nachbedingung *)RETURN z;END Multiply;Die Schnittstelle dieser Funktionsprozedur würde dann zum Beispiel wie folgt aussehen:32 Lei<strong>der</strong> bestehen zwischen verschiedenen Oberon-2-Compilern Unterschiede, was den für diesen Wertzulässigen Bereich angeht. Die im folgenden angegeben Konventionen gelten daher nur für Oberon-2-Compiler <strong>der</strong> ETH Zürich, die auf OP2 basieren.33 <strong>Zur</strong> Generierung einer Beschreibung <strong>der</strong> Schnittstelle eines Moduls existieren auch Werkzeuge, diees erlauben, gewisse Kommentare mit in diese Beschreibung zu übernehmen. Üblicherweise werdensolche exportierten Kommentare“ dadurch gekennzeichnet, daß sie mit (**“ statt mit (*“ beginnen.” ” ”


98 Spezifikation, Axiomatische Semantik und KorrektheitPROCEDURE Multiply (x, y: INTEGER): INTEGER;(**REQUIRE (x >= 0) & (y >= 0);ENSURE RESULT = x * y;*)4.5.3 Invarianten und Varianten für SchleifenAuch für Invarianten und Varianten von Schleifen kann die Prozedur ASSERT() genutztwerden. Für Invarianten verwendet man einfach den entsprechenden Ausdruck <strong>der</strong> Zusicherung.Für Varianten muß allerdings nicht ein Boolscher Ausdruck geprüft werden,vielmehr muß sichergestellt sein, daß <strong>der</strong> entsprechende ganzzahlige Ausdruck strengmonoton fällt.Hierzu kann man zum Beispiel eine Funktionsprozedur verwenden, die entsprechendeVergleiche durchführt und sich das Resultat des letzten Schleifendurchlaufs über einenSeiteneffekt ”merkt“:PROCEDURE EnsureDecrease (expr: INTEGER;VAR temp: INTEGER): BOOLEAN;VAR okay: BOOLEAN;BEGINokay := (expr < temp) & (expr >= 0);IF okay THEN temp := expr; END;RETURN okay;END EnsureDecrease;PROCEDURE InitTemp (VAR temp: INTEGER);BEGINtemp := MAX(INTEGER);END InitTemp;Die Prozedur InitTemp() dient lediglich <strong>der</strong> Initialisierung <strong>der</strong> Variable, in <strong>der</strong> die temporärenWerte gespeichert werden sollen. Zu beachten ist, daß angesichts geschachtelterSchleifen für jede Variante eine entsprechende temporäre Variable nötig ist.Beispiel: Mit diesen Mitteln könnten wir die obige Funktionsprozedur nun wie folgtschreiben:PROCEDURE Multiply* (x, y: INTEGER): INTEGER;(**REQUIRE (x >= 0) & (y >= 0);ENSURE RESULT = x * y;*)VARu: INTEGER; z: INTEGER; temp: INTEGER;BEGINASSERT ((x >= 0) & (y >= 0), 100);InitTemp (temp);z := 0; u := y;WHILE u > 0 DOASSERT (z + u * x = x * y,110); (* Invariante *)ASSERT (EnsureDecrease(u,temp),111); (* Variante *)


4.5 Alternativen in Oberon-2 99z := z + x; u := u - 1ENDASSERT (z = x * y, 120);RETURN z;END Multiply;4.5.4 Invarianten für Klassen und ModuleInvarianten für Klassen und Module können wir ebenfalls durch geeignete ASSERT()-Aufrufe zur Laufzeit prüfen:• Im Fall von Klassen deklarieren wir eine Methode, in <strong>der</strong> die Invarianten <strong>der</strong> Klassegeprüft werden.• Im Fall von Modulen deklarieren wir eine Prozedur, in <strong>der</strong> die Invarianten desModuls geprüft werden.• Für abstrakte Datenstrukturen, also ”Klassen-ähnliche“ Implementierungen ohneMethoden, deklarieren wir eine Prozedur, in <strong>der</strong> die Invarianten des entsprechendenTyps geprüft werden.Das Ergebnis <strong>der</strong> Prüfung wird jeweils als Ergebnis <strong>der</strong> Methode bzw. <strong>der</strong> Prozedurzurückgegeben. Das Ergebnis dieser Prozeduren bzw. Methoden wird dann in allenan<strong>der</strong>en Prozeduren bzw. Methoden, die entsprechende Invarianten verletzen könnten,durch einen Aufruf von ASSERT() geprüft.Beispiel: Wir betrachten wie<strong>der</strong> den abstrakten Datentyp Stack und verschiedeneImplementierungen (wie in Abschnitt 4.3.3 beschrieben durch Arrays) für diesen. Füreine Implementierung durch eine Klasse deklarieren wir die folgende Methode:PROCEDURE (self: Stack) Invariant* (): BOOLEAN;(**INVARIANT 0


100 Spezifikation, Axiomatische Semantik und KorrektheitBEGINRETURN 0


4.5 Alternativen in Oberon-2 1014.5.5 ZusammenfassungZusammenfassend können wir festhalten, daß wir eine Teilmenge <strong>der</strong> in Abschnitt 4.2und Abschnitt 4.3 eingeführten Konzepte auch direkt in Oberon-2 anwenden können.Die vordeklarierte Prozedur ASSERT() bietet hierfür eine geeignete Grundlage, sofernsie ähnlich, wie in diesem Abschnitt beschrieben, eingesetzt wird. Eine zufriedenstellendenUmsetzung aller Konzepte können wir so allerdings aus folgenden Gründen nichterreichen:• Die ”doppelte“ Spezifikation durch Kommentare und ASSERT()-Aufrufe führt eineunerwünschte Redundanz ein.• Zusicherungen können in dieser Form nicht durch den Compiler für Optimierungenherangezogen werden, da sie nicht Teil <strong>der</strong> Programmiersprache selbst sind.• Ein Klient hat unter Umständen keine Möglichkeit, sich aus <strong>der</strong> Schnittstelle desModuls über die erwarteten und garantierten Bedingungen zu informieren.Die Verwendung von ASSERT() stellt einen Kompromiß zwischen unserem Wunsch nachformal spezifizierter und korrekter Software und den praktischen Einschränkungen einereinfachen“ Programmiersprache wie Oberon-2 und des Compilers OP2 dar. Bis durch”entsprechende Erweiterungen (an Oberon-2 selbst für Konstruktoren sowie im Compilerfür den Zugriff auf Syntaxbäume externer Module) die Grundlagen für eine bessereIntegration dieser Konzepte geschaffen worden sind, sollten wir diesen Kompromiß aberakzeptieren und — nicht zuletzt im Sinne <strong>der</strong> Anwen<strong>der</strong> unserer Programme — damitarbeiten.


Kapitel 5Prozeduren, Funktionen undParameterübergabeThe first programmable computer, Babbage’s AnalyticalEngine, built in the 1840s, had the capacity of reusingcollections of instruction cards at several different placesin a program when that was convenient. ...Instead ofexplaining how some computation is to be done ...thatexplanation is enacted by a call statement . . .— Robert W. Sebesta, in [56]Don’t call us, we’ll call you!— Herkunft unbekanntIn diesem Kapitel beschäftigen wir uns näher mit den in Oberon-2 vorhandenen Konzeptenfür Prozeduren und Funktionen sowie mit den Mechanismen zur Übergabe vonParametern. Wir zeigen zunächst, daß die Verwendung von Funktionsprozeduren undReferenzparametern zu einer Vielzahl von Problemen führen kann. Um diese Problemezu beseitigen, entwickeln wir für beide Bereiche sichere Alternativen. Anschließend diskutierenwir den Entwurf entsprechen<strong>der</strong> Spracherweiterungen und ihre Integration inden Compiler OP2.


104 Prozeduren, Funktionen und Parameterübergabe5.1 Einführung und Motivation5.1.1 Parameterübergabe: Spezifikation und ImplementierungProzeduren dienen, wie in Abschnitt 2.3 kurz angesprochen, als Abstraktionen für Folgenvon Anweisungen. Sie können (in einem gewissen Rahmen) wie<strong>der</strong>verwendet werden, dasie es erlauben, die in ihnen gekapselten Anweisungsfolgen an <strong>der</strong> Stelle ihres Aufrufs zuparametrisieren. Eine Prozedur zur Multiplikation zweier Matrizen macht es beispielsweisemöglich, ein entsprechendes Verfahren einmal zu implementieren und an vielenStellen (unter Umständen in an<strong>der</strong>en Modulen) zu verwenden.Die Schnittstelle (Signatur) einer Prozedur spezifiziert neben dem Typ ihrer Parameterauch die Richtung des über diese Parameter stattfindenden Datenflusses. Siesollte also Auskunft darüber geben, ob ein Parameter zur Eingabe o<strong>der</strong> zur Ausgabevon Daten o<strong>der</strong> für beides verwendet wird:• Über Eingabeparameter wird die aufgerufene Prozedur lediglich mit einem bestimmtenWert versorgt, <strong>der</strong> für ihren Ablauf relevant ist.• Über Ausgabeparameter werden Werte, die innerhalb <strong>der</strong> Prozedur berechnet wurden,an ihren Aufrufer zurückgegeben.• Über Ein- und Ausgabeparameter kann die aufgerufene Prozedur beides erreichen:Sie wird zum einen durch den Aufrufer mit einem bestimmten Wert versorgt, kannaber zum an<strong>der</strong>en auch ein Ergebnis an diesen Aufrufer zurückgeben.Die Schnittstelle <strong>der</strong> oben angesprochenen Prozedur zur Multiplikation zweier Matrizenwürden wir (in einer fiktiven, durch Oberon-2 und Ada inspirierten Programmiersprache)etwa wie folgt beschreiben:PROCEDURE Multiply (IN a: ARRAY OF ARRAY OF REAL;IN b: ARRAY OF ARRAY OF REAL;OUT c: ARRAY OF ARRAY OF REAL);Anhand dieser Schnittstelle würde ein Klient <strong>der</strong> Prozedur beispielsweise erkennen, daßdie Parameter a und b innerhalb <strong>der</strong> Prozedur ausschließlich gelesen werden, während<strong>der</strong> Parameter c ausschließlich beschrieben wird. Weiterhin könnte dieser Klient auchdarauf schließen, daß die Parameter a und b vor dem Aufruf <strong>der</strong> Prozedur initialisiertwerden müssen, während dies für den Parameter c nicht notwendig ist. Die Kennzeichnungdes Datenflusses, <strong>der</strong> über diese Parameter stattfindet, hat also Konsequenzenfür den Aufrufer <strong>der</strong> Prozedur. Natürlich könnte die Prozedur auch mit <strong>der</strong> folgendenSchnittstelle deklariert werden:PROCEDURE Multiply (INOUT a: ARRAY OF ARRAY OF REAL;INOUT b: ARRAY OF ARRAY OF REAL;INOUT c: ARRAY OF ARRAY OF REAL);


5.1 Einführung und Motivation 105Dann müßten wir für den Klienten allerdings entsprechende Kommentare hinzufügen,die den jeweiligen Datenfluß genauer beschreiben (zum Beispiel in <strong>der</strong> Form (* c :=a * b *)). Die ursprüngliche Schnittstelle ist also klar zu bevorzugen, da sie genauereAussagen macht. 1In Oberon-2 istesaberlei<strong>der</strong>nichtmöglich, eine solche Spezifikation <strong>der</strong> Schnittstelleeiner Prozedur anzugeben. Wir haben lediglich die Möglichkeit, uns zwischen zwei Arten<strong>der</strong> Parameterübergabe zu entscheiden:• Formale Wertparameter (value substitution, call by value): Zugehörige aktuelle Parameterkönnen Ausdrücke sein, die vor Aufruf <strong>der</strong> Prozedur ausgewertet werden.Ihr Wert wird lokal in <strong>der</strong> Prozedur gespeichert.• Formale Referenzparameter (variable substitution, call by reference): Zugehörigeaktuelle Parameter müssen Variablen sein. Der Name des formalen Parametersdient innerhalb <strong>der</strong> Prozedur als lokaler Alias für diese Variable.Ein Vergleich mit den oben eingeführten Bezeichnungen zeigt, daß Wertparameter ausschließlichEingabeparameter sind. Zusätzlich wird für sie auch eine lokale Kopie erzeugt,so daß Zuweisungen an einen formalen Wertparameter keinen Einfluß auf den Wert desaktuellen Parameters außerhalb <strong>der</strong> Prozedur haben. Referenzparameter sind dagegenstets Ein- und Ausgabeparameter. Sie können zwar innerhalb <strong>der</strong> Prozedur auch ausschließlichals Eingabe- o<strong>der</strong> Ausgabeparameter verwendet werden, dies ist aber aus <strong>der</strong>Schnittstelle <strong>der</strong> Prozedur nicht ersichtlich. Ein Klient ist also auf die Einhaltung vonKonventionen angewiesen, die <strong>der</strong> Entwickler <strong>der</strong> Prozedur festlegt hat.Durch die Kennzeichnung als Wert- o<strong>der</strong> Referenzparameter wird in Oberon-2 außerdemauch ein konkreter Mechanismus für die Parameterübergabe festgelegt. In <strong>der</strong> obenverwendeten fiktiven Programmiersprache bliebe die Wahl dieses Mechanismus offen.Ein Compiler könnten zum Beispiel für einen INOUT-Parameter sowohl call by referenceals auch call by value-result (siehe [56]) verwenden. Für einen IN-Parameter hätte erdagegen die Wahl zwischen call by value und call by reference.Diese direkte Bindung an eine konkrete Implementierung <strong>der</strong> Parameterübergabeführt in Oberon-2 zu Problemen. Betrachten wir als Beispiel wie<strong>der</strong> eine Prozedur zurMultiplikation zweier Matrizen. Die folgende Schnittstelle würde den über die Parameterstattfindenden Datenfluß am besten beschreiben:PROCEDURE Multiply (a: ARRAY OF ARRAY OF REAL;b: ARRAY OF ARRAY OF REAL;VAR (*OUT*) c: ARRAY OF ARRAY OF REAL);Da die Parameter a und b lediglich zur Eingabe dienen werden sie als Wertparameterdeklariert. Der Parameter c dagegen dient zwar lediglich zur Ausgabe, muß aber —1 Vielen Dank an Claudio Nie<strong>der</strong>, <strong>der</strong> auf diese Bedeutung des Datenflusses hingewiesen hat.Die Konsequenzen für den Aufrufer könnten wir auch als eine Erweiterung des in Abschnitt 4.1.4eingeführten Vertraglichen Programmierens verstehen: Es wird festgelegt, wann eine Initialisierungdurch den Klienten von Bedeutung ist und wann nicht.


106 Prozeduren, Funktionen und Parameterübergabemangels an<strong>der</strong>er Möglichkeiten — als Referenzparameter deklariert werden. 2Durch diese Schnittstelle wird dem Klienten vor allem garantiert, daßdiedenformalenParametern a und b entsprechenden aktuellen Parameter nicht verän<strong>der</strong>t werden.Allerdings ist eine Prozedur mit dieser Schnittstelle im allgemeinen wenig effizient: Da aund b Wertparameter sind, müssen beim Aufruf <strong>der</strong> Prozedur lokale Kopien <strong>der</strong> aktuellenParameter angelegt werden. Dies ist für große Matrizen eine teuere Operation.Aus diesem Grund würden wir die Prozedur wahrscheinlich eher mit <strong>der</strong> folgendenSchnittstelle deklarieren:PROCEDURE Multiply (VAR (*IN*) a: ARRAY OF ARRAY OF REAL;VAR (*IN*) b: ARRAY OF ARRAY OF REAL;VAR (*OUT*) c: ARRAY OF ARRAY OF REAL);Für Referenzparameter müssen keine Kopien <strong>der</strong> aktuellen Parameter angelegt werden.Aufrufe <strong>der</strong> Prozedur sind nun also effizienter. Lei<strong>der</strong> kann dem Klienten jetzt abernicht mehr garantiert werden, daß die den formalen Parametern a und b entsprechendenaktuellen Parameter unverän<strong>der</strong>t bleiben: Dem Compiler ist ja nicht einmal bekannt, daßsie per Konvention lediglich als Eingabeparameter dienen!Wir erkennen also, daß es wünschenswert wäre, dem Compiler mehr Informationenüber den durch einen Parameter modellierten Datenfluß zu geben. Darüber hinaus wärees auch sinnvoll, ihm mehr Freiheiten bei <strong>der</strong> Wahl <strong>der</strong> Implementierung <strong>der</strong> Parameterübergabezu lassen.5.1.2 Seiteneffekte in FunktionsprozedurenFunktionsprozeduren dienen, wie in Abschnitt 2.3 kurz angesprochen, als Abstraktionenfür Ausdrücke. Dort wurde ebenfalls darauf hingewiesen, daß Ausdrücke im allgemeinenfrei von Seiteneffekten sind. Deswegen sollten insbeson<strong>der</strong>e auch Aufrufe von Funktionsprozeduren(die ja nur in Ausdrücken auftreten) keine Seiteneffekte haben.Auf zwei Probleme, die durch Seiteneffekte von Funktionsprozeduren entstehen, sindwir in Abschnitt 4.2.7 und Abschnitt 4.3.4 eingegangen:• Korrektheitsbeweise im Rahmen <strong>der</strong> axiomatischen Semantik gehen davon aus, daßAusdrücke frei von Seiteneffekten sind. Sind sie dies nicht (etwa durch die Verwendungvon Funktionsprozeduren), so gelten im allgemeinen auch die Beweisregelnnicht mehr vollständig.• Ausdrücke, die wir in Zusicherungen verwenden, sollten frei von Seiteneffektensein, da sie eine deklarative Natur haben. Klar wird dies, wenn wir in Betrachtziehen, daß die Prüfung <strong>der</strong> Zusicherungen durch eine Compileroption ausgeschaltetwerden kann: Wenn die betroffenen Ausdrücke Seiteneffekte verursachen, dannhängt <strong>der</strong> Ablauf des Programms unter Umständen von dieser Compileroption ab.2 Es ist guter Stil, die Art des Datenflusses über einen Referenzparameter durch einen Kommentarzu kennzeichnen. Üblicherweise werden dazu die aus Ada bekannten Schlüsselwörter IN, OUT und INOUTverwendet.


5.1 Einführung und Motivation 107Das in Abschnitt 4.2.7 gegebene Beispiel illustriert aber das zentrale Problem von Seiteneffektenin Funktionsprozeduren: Dort liefert <strong>der</strong> Ausdruck Side(1) = Side(1) stetsden Wert FALSE! Aufrufe von Funktionsprozeduren sind also im allgemeinen nicht referentielltransparent: IdentischeAufrufe einer Funktionsprozedur führen nicht unbedingtauch zu identischen Resultaten.Nach [27] können wir Funktionsprozeduren im Hinblick auf die von ihnen erzeugtenSeiteneffekte wie folgt klassifizieren:• R-Funktionen dürfen keine nicht-lokalen Variablen verän<strong>der</strong>n. Solche Funktionensind vollständig frei von Seiteneffekten. Für einen gegebenen (globalen) Zustanddes Programms und gegebene Parameter erzeugt eine solche Funktion stets dasgleiche Ergebnis.• S-Funktionen dürfen keine nicht-lokalen Variablen verän<strong>der</strong>n, die schon vor demAufruf <strong>der</strong> Funktion existiert haben. Solche Funktionen modifizieren den globalenZustand nicht, sie können ihn aber durch neue, dynamisch angelegte Variablen erweitern.Solange (eventuell durch sie zurückgegebene) Zeigerwerte nicht verglichenwerden, sind sie frei von Seiteneffekten.• N-Funktionen dürfen jede in ihnen sichtbare Variable verän<strong>der</strong>n und erlauben sobeliebige Seiteneffekte.Offensichtlich sind die in Oberon-2 vorhandenen Funktionsprozeduren nach dieser KlassifikationN-Funktionen.Um die Möglichkeit zu schaffen, Seiteneffekte innerhalb von Ausdrücken zu vermeiden,würden wir gerne S-Funktionen bzw. R-Funktionen in <strong>Fro<strong>der</strong>on</strong>-1 integrieren.Bezüglich <strong>der</strong> S-Funktionen zeigt Robert Griesemer aber in [27], daß wir <strong>der</strong>en Freiheitvon Seiteneffekten nur durch Prüfungen zur Laufzeit garantieren können. 3 Dawir aber ungern weitere Laufzeitprüfungen einführen würden und uns außerdem vonR-Funktionen aus auch <strong>der</strong> Weg zu S-Funktionen offen bleibt (etwa für spätere Revisionenvon <strong>Fro<strong>der</strong>on</strong>-1), wollen wir also die Möglichkeit schaffen, R-Funktionen direkt in<strong>Fro<strong>der</strong>on</strong>-1 deklarieren zu können.Wir merken hier noch an, daß R-Funktionen bzw. S-Funktionen und die mit ihnenverbundene Seiteneffektfreiheit von Ausdrücken auch an<strong>der</strong>e Anwendungen (außerhalbvon Korrektheitsbeweisen) haben: In [26] und [5] wird beispielsweise gezeigt, daß durchsie bessere Möglichkeiten für optimierende Compiler geschaffen werden, da die Reihenfolge<strong>der</strong> Auswertung eines Ausdrucks irrelevant wird.3 Jedenfalls dann, wenn wir nicht auf Verfahren <strong>der</strong> Datenflußanalyse zurückgreifen wollen, die dieKomplexität des Compilers stark erhöhen würden.


108 Prozeduren, Funktionen und Parameterübergabe5.2 Spracherweiterungen5.2.1 Konstante ProzedurparameterIn Abschnitt 5.1.1 wurde gezeigt, daß uns die zwei in Oberon-2 vorhandenen Mechanismenzur Parameterübergabe für viele Prozeduren zu einer Entscheidung zwischenEffizienz und Sicherheit zwingen. In <strong>Fro<strong>der</strong>on</strong>-1 könnten wir dieses Problem auf verschiedeneArten lösen, wir konzentrieren uns hier aber auf die folgenden Vorschläge:1. Wir ersetzen das bisherige Konzept <strong>der</strong> Parameterübergabe vollständig durch einan Ada angelehntes.2. Wir erweitern Referenzparameter um die Möglichkeit, den gewünschten Datenflußexplizit anzugeben.3. Wir führen lediglich einen neuen Mechanismus zur Parameterübergabe ein, <strong>der</strong>einem ”schreibgeschützten Referenzparameter“ entspricht.Je<strong>der</strong> dieser Vorschläge hat natürlich sowohl Vor- als auch Nachteile: Während <strong>der</strong> ersteVorschlag zwar unsere Probleme am besten löst (die Parameter würden wie in <strong>der</strong> fiktivenSprache aus Abschnitt 5.1.1 angegeben), ist er aber lei<strong>der</strong> in keiner Weise kompatibelmit Oberon-2. Esbestünde nur die Möglichkeit, die neuen Parametermodi IN, OUT undINOUT zusätzlich zu den schon vorhandenen aufzunehmen. Mit Blick auf das ”Ideal <strong>der</strong>Einfachheit“ sehen wir davon aber ab.Der zweite Vorschlag dagegen wäre kompatibel mit Oberon-2, da wir damit lediglicheine optionale Angabe des Datenflusses von Referenzparametern ermöglichen. Statt VARkönnten wir nun VAR IN, VAR OUT sowie VAR INOUT verwenden, wobei letzteres gleichbedeutendmit <strong>der</strong> in Oberon-2 üblichen Interpretation von VAR wäre. Wenn wir minimalistischvorgehen wollen, wäre VAR INOUT also redundant. Referenzparameter <strong>der</strong> FormVAR IN könnten nun in gewünschter Weise sicherstellen, daß die Parameterübergabe effizientund sicher (im Sinne von ”<strong>der</strong> Spezifikation des Datenflusses genügend“) erfolgt.Allerdings würde VAR OUT lediglich <strong>der</strong> Dokumentation <strong>der</strong> Schnittstelle dienen, da wirfür solche Referenzparameter keine neuen Regeln o<strong>der</strong> Prüfungen angeben können. Wirmüßten außerdem zumindest zwei neue Schlüsselwörter in <strong>Fro<strong>der</strong>on</strong>-1 aufnehmen. 4Der dritte und letzte Vorschlag deckt dagegen genau den interessanten Teil des zweitenVorschlags ab: Er entspricht <strong>der</strong> Einführung von VAR IN. Außerdem können wir inAnlehnung an das Schlüsselwort VAR für ”normale“ Referenzparameter das SchlüsselwortCONST wie<strong>der</strong>verwenden, müssen also kein neues Schlüsselwort einführen. 5 Da wir durchdiesen Vorschlag mit den wenigsten Än<strong>der</strong>ungen am meisten erreichen können, nehmenwir ihn in <strong>Fro<strong>der</strong>on</strong>-1 auf. Die notwendigen Än<strong>der</strong>ungen an <strong>der</strong> Syntax von Prozedurdeklarationenbetreffen lediglich eine Produktion:4 Stilistisch gesehen ist es darüber hinaus in Oberon und Oberon-2 unüblich, zwei Schlüsselwörterin Folge“ anzugeben.” 5 Es gibt auch einen an<strong>der</strong>en Vorschlag für die konkrete Syntax dieser Erweiterung, <strong>der</strong> sich an <strong>der</strong>Syntax des schreibgeschützten Exports orientiert. Siehe Anmerkung 5.1.


5.2 Spracherweiterungen 109FPSection = [VAR|CONST ] ident "," ident ":" Type.Die Schnittstelle <strong>der</strong> in Abschnitt 5.1.1 angesprochenen Prozedur zur Multiplikationzweier Matrizen hätte damit in <strong>Fro<strong>der</strong>on</strong>-1 die folgende Form:PROCEDURE Multiply (CONST a: ARRAY OF ARRAY OF REAL;CONST b: ARRAY OF ARRAY OF REAL;VAR (*OUT*) c: ARRAY OF ARRAY OF REAL);Durch sie erreichen wir sowohl eine effiziente Übergabe als Referenzparameter als auchSicherheit bezüglich <strong>der</strong> Semantik von reinen Eingabeparametern.Anmerkung 5.1 Schreibgeschützte Parameter in den Oakwood GuidelinesDas in diesem Abschnitt behandelte Problem bei <strong>der</strong> Parameterübergabe ist natürlich auch vonan<strong>der</strong>en erkannt worden. In den Oakwood Guidelines [38] findet sich im Abschnitt Read-onlyVAR Parameters folgendes:There have been many requests to make ARRAY and RECORD parameters read-onlyto achieve the efficiency of passing by reference without the associated possibilityfor corruption of the calling parameter. An attempt to make an assignment to anycomponent of such a read-only parameter is a compile-time error. Such parameterscould be marked with the standard read-only “-” symbol. For example:PROCEDURE Print (theText-:ARRAY OF CHAR);Discussions with ETH suggest this is really a compiler code optimisation issue andon this basis it is recommended that this extension is not implemented.Bis auf die verwendete Syntax ist dieser Vorschlag weitgehend mit unserem identisch. Allerdingsist die aus Diskussionen mit <strong>der</strong> ETH“ gezogene Schlußfolgerung, es handle sich um”ein reines Optimierungsproblem“, nicht zufriedenstellend. Natürlich wäre es möglich, den”Compiler für jeden Wertparameter entscheiden zu lassen, ob auch eine Übergabe als Referenzparametermöglich ist, ohne die Semantik <strong>der</strong> Prozedur zu verän<strong>der</strong>n (ein Traversieren desSyntaxbaums <strong>der</strong> Prozedur würde genügen, um für jeden Parameter zu entscheiden, ob Zuweisungenan ihn erfolgen, o<strong>der</strong> ob er als Referenzparameter an an<strong>der</strong>e Prozeduren übergebenwird). Problematisch ist nur, daß die Optimierung“ nicht Teil <strong>der</strong> Sprachdefinition ist: Der”Programmierer müßte sich anhand <strong>der</strong> Implementierung eines konkreten Compilers entscheiden,ob er einen Wertparameter (<strong>der</strong> dann auf einen Referenzparameter abgebildet wird) o<strong>der</strong>einen Referenzparameter verwendet, um eine effiziente Lösung zu erhalten. Die explizite Kennzeichung<strong>der</strong> gewünschten Semantik (und damit des gewünschten Datenflusses) bringt aberauch einen <strong>weiteren</strong> Vorteil mit sich: Würde zum Beispiel in <strong>der</strong> Prozedur für Matrizenmultiplikationversehentlich“ eine Zuweisung an a o<strong>der</strong> b gemacht, so würde ohne explizite”Kennzeichnung lediglich die Optimierung nicht mehr greifen können, es würde aber kein Fehlergemeldet werden können. Dagegen kann bei einer expliziten Kennzeichnung sehr wohl eineaussagekräftige Fehlermeldung erzeugt werden. Implizite Mechanismen wi<strong>der</strong>sprechen außerdem<strong>der</strong> Philosophie von Oberon-2, den Programmierer möglichst viel explizit ausdrücken zulassen.


110 Prozeduren, Funktionen und ParameterübergabeEs sind aber immer noch einige Details dieser Spracherweiterung offen, die wir zuihrer Formalisierung und Implementierung klären müssen:1. Wie soll CONST für Parameter eines Zeigertyps interpretiert werden? Ist <strong>der</strong> Zeiger,das referenzierte Objekt o<strong>der</strong> sind beide konstant, das heißt schreibgeschützt?2. Ist CONST für Empfängerparameter bei typgebundenen Prozeduren zulässig?3. Sollen strukturierte und einfach Typen wirklich gleich behandelt werden? Sollalso immer ein Referenzparameter übergeben und damit eine Indirektion in Kaufgenommen werden?4. Wie sollen ”strukturierte Konstanten“, die in Oberon-2 in <strong>der</strong> Form von Zeichenkettenauftreten, behandelt werden? Sie dürfen in Oberon-2 ja nicht als Referenzparameterübergeben werden.Wenden wir uns also zunächst <strong>der</strong> ersten Frage zu, und betrachten wir dabei die sicherlichhäufigste Anwendung von Zeigern in Oberon-2. Da Zeiger vorwiegend dazu verwendetwerden, polymorphe Referenzen auf Objekte (in Sinn <strong>der</strong> Objektorientierung) zu halten,wäre es sicherlich sinnvoll, die referenzierten Objekte als schreibgeschützt zu betrachten.An<strong>der</strong>erseits verträgt sich diese Interpretation von CONST nicht mit den sehr ähnlichenRegeln für schreibgeschützt (also durch ”-“) exportierte Bezeichner: Dort ist nämlich nur<strong>der</strong> Zeiger selbst, nicht aber das durch ihn referenzierte Objekt schreibgeschützt. Aberauch diese, an Oberon-2 angelehnte Interpretation hat ihre Probleme, wie folgendesBeispiel verdeutlicht:PROCEDURE Paradox (CONST a: ARRAY OF CHAR;CONST b: POINTER TO ARRAY OF CHAR);BEGINa[0] := "A"; b[0] := "B";END Paradox;Hier sollte man annehmen, daß die identisch formulierten Zuweisungen a[0] := "A"und b[0] := "B" auch identische Wirkungen haben. Allerdings führt bei dieser Interpretationdie Zuweisung a[0] := "A" zu einem — durch den Compiler gemeldeten —Fehler, während die Zuweisung b[0] := "B" das Array b wirklich verän<strong>der</strong>t. 6 Wennwir dagegen sowohl den Zeiger als auch das referenzierte Objekt als schreibgeschütztbetrachten, würden in obigem Beispiel wenigstens symmetrisch“ beide Zuweisungen zu”einer Fehlermeldung führen. Ein Argument gegen die Betrachtungsweise, daß Zeiger undreferenziertes Objekt schreibgeschützt sein sollten, ist aber auch das Folgende: Wenn innerhalbeiner Prozedur ein durch CONST gekennzeichneter Parameter eines Zeigertyps aneinen kompatiblen, aber lokal deklarierten Zeiger zugewiesen wird, dann müßten wir den” Schreibschutz“ an diesen Zeiger weiterreichen“ o<strong>der</strong> alternativ die Zuweisung selbst” 6 Natürlich tritt dieses Problem in ähnlicher Form auch für den schreibgeschützten Export auf.


5.2 Spracherweiterungen 111verbieten. Dies wäre zwar möglich, würde sich aber nicht gut in die an<strong>der</strong>en Regeln fürZuweisungskompatiblität zwischen zwei Typen einfügen. Angesichts dieser Probleme istes also keinesfalls klar, welche Interpretation wir bevorzugen sollten. Wir orientierenuns deswegen für <strong>Fro<strong>der</strong>on</strong>-1 an <strong>der</strong> Interpretation des schreibgeschützten Exports undbetrachten den Zeiger, nicht aber das referenzierte Objekt als schreibgeschützt.Nach dieser Diskussion können wir nun auch leichter die Frage beantworten, ob CONSTfür Empfängerparameter zulässig sein sollte. Hier ist offensichtlich, daß die sinnvollste“Interpretation eines schreibgeschützten Empfängers“ die Aussage Diese Methode” ””verän<strong>der</strong>t das Objekt nicht“ ist. 7 Da wir uns oben dafür entschieden haben, im Fallvon Zeigertypen den Zeiger selbst, nicht aber das referenzierte Objekt als konstant zubetrachten, sollte klar sein, daß CONST nur dann für einen Empfängerparameter zulässigsein darf, wenn dieser ein Recordtyp ist: Nur dann kann er innerhalb <strong>der</strong> Methode auchnicht verän<strong>der</strong>t werden. Natürlich müssen wir hier eine weitere Modifikation an <strong>der</strong>Syntax von Oberon-2 vornehmen:Receiver = "(" [VAR|CONST ] ident ":" ident ")".Durch sie ist es jetzt möglich, auch Empfängerparameter als schreibgeschützt zu deklarieren.Der Compiler muß außerdem sicherstellen, daß <strong>der</strong> Empfängerparameter einenRecordtyp hat.Wenden wir uns nun <strong>der</strong> dritten Frage zu. Wir haben oben den neuen ParametermodusCONST als schreibgeschützten Referenzparameter“ eingeführt. Für elementare”Typen erscheint ein Referenzparameter an dieser Stelle aber unnötig und sogar ineffizient:Wir nehmen dadurch eine Indirektion in Kauf, obwohl sie eigentlich nicht notwendigwäre. Wenn wir für elementare Typen also CONST als schreibgeschützten Wertparameter“behandeln würden, dann könnten wir sowohl diesen Umweg“ vermeiden als auch””an <strong>der</strong> Stelle des Aufrufs Ausdrücke verwenden. Bis jetzt wären dort auch für elementareTypen immer Variablen notwendig. Da diese Modifikation keine an<strong>der</strong>en (negativen)Konsequenzen nach sich zu ziehen scheint, wollen wir die Entscheidung, ob Wert- undReferenzparameter verwendet werden sollen, also von dem Typ des Parameters abhängigmachen und sie damit dem Compiler überlassen. 8Die vierte Frage schließlich behandelt ein ähnliches Problem. Da Parameter strukturierterTypen stets als Referenzparameter übergeben werden, ist es nicht möglich, füreinen formalen Parameter CONST s: ARRAY OF CHAR, beimAufrufeineZeichenkette <strong>der</strong>Form "Dies ist eine Zeichenkette" zu übergeben. 9 Wir stehen also vor <strong>der</strong> Wahl,dem Programmierer zuzumuten“, für Zeichenketten stets Variablen zu verwenden o<strong>der</strong>”eine temporäre Variable automatisch durch den Compiler erzeugen zu lassen. 10 Da wires aber vermeiden wollen, implizite Mechanismen in <strong>Fro<strong>der</strong>on</strong>-1 einzubringen, verzichten7 In C++ können Methoden auch als konstant“ gekennzeichnet werden, wobei genau diese Interpretationunterstellt wird.8 In Ada wird eine ähnliche Entscheidung ebenfalls dem Compiler überlassen (siehe [56, S. 343f]).9 Allgemeiner tritt dieses Problem für strukturierte Konstanten“ auf. In Oberon-2 kommen struk-””turierte Konstanten aber nur in Form von Zeichenketten vor.10 In [26] führt Robert Griesemer in Oberon-V für ein ähnliches Problem ebenfalls automatischtemporäre Variablen ein.


112 Prozeduren, Funktionen und Parameterübergabewir auf die automatische Einführung von Variablen und erlauben folglich eine Übergabevon Zeichenketten an Parameter vom Modus CONST nicht.Anmerkung 5.2 Konstante Parameter und AliasingWir wollen hier nochmals kurz auf das (schon in Abschnitt 4.2.7 angesprochene) Problem desAliasing eingehen. Von Aliasing sprechen wir immer dann, wenn eine Variable durch zweio<strong>der</strong> mehr Bezeichner ansprechbar ist. Als Beispiel betrachten wir einen Aufruf <strong>der</strong> ProzedurMultiply() in <strong>der</strong> Form Multiply (x, x, x). Sowohl für Referenzparameter in Oberon-2als auch für konstante Parameter in <strong>Fro<strong>der</strong>on</strong>-1 würde ein solcher Aufruf ein falsches Ergebnisliefern, da in <strong>der</strong> Prozedur die lokalen Bezeichner a, b und c an eine Variable gebunden sind.Durch die konstanten Parameter können wir Aliasing in <strong>Fro<strong>der</strong>on</strong>-1 allerdings in vielen Fällenverbieten: Da wir wissen, welche Referenzparameter lediglich zur Eingabe dienen, können wirdie Diskjunktheitsbedingung aus Abschnitt 4.2.5 im Compiler überprüfen. Der Aufruf Multiply(x, x, x) würde dann zu einer Fehlermeldung führen, ebenso wie <strong>der</strong> Aufruf Multiply (x,y, x). Hingegen wären die Aufrufe Multiply (x, x, y) und Multiply (x, y, z) zulässig.Ohne auf Verfahren <strong>der</strong> Datenflußanalyse zurückzugreifen können wir aber auch in <strong>Fro<strong>der</strong>on</strong>-1nicht alle Möglichkeiten Aliasing zu erzeugen abfangen.5.2.2 Seiteneffektfreie FunktionenDie Einführung seiteneffektfreier Funktionen in <strong>Fro<strong>der</strong>on</strong>-1 wurde in Abschnitt 5.1.2motiviert. Um entsprechende Spracherweiterungen zu formulieren, müssen wir zunächstentscheiden, in welcher Form solche Funktionen gekennzeichnet werden sollen. Der Einfachheithalber verwenden wir dazu das aus Pascal bekannte Schlüsselwort FUNCTION.Die in Pascal durch die Verwendung von FUNCTION zur Kennzeichung von Funktionsprozeduren(im Sinne von N-Funktionen) auftretenden Verwirrungen sind für <strong>Fro<strong>der</strong>on</strong>-1aber nicht zu befürchten: Hier sind FUNCTIONs jaR-Funktionen, alsoechte Funktionenim mathematischen Sinn.Syntaktisch müssen wir also eine neue Produktion für Funktionen einführen, die sichweitgehend an <strong>der</strong> bestehenden Produktion für Prozeduren und Funktionsprozedurenorientiert. Zwei Unterschiede können wir jedoch festhalten:• Im Gegensatz zu Prozeduren und Funktionsprozeduren dürfen Funktionen keineReferenzparameter (im Sinne von VAR) erwarten. Diese würden ja wie<strong>der</strong>um Seiteneffekteermöglichen.• Die Angabe eines Typs für das Ergebnis <strong>der</strong> Funktion ist nicht länger optional, dajede Funktion ja eine Abbildung im mathematischen Sinn darstellen soll.Dies führt uns zu den folgenden — relativ umfangreichen — Än<strong>der</strong>ungen <strong>der</strong> Syntax:DeclSeq ={CONST {ConstDecl ";"} | TYPE {TypeDecl ";"} | VAR {VarDecl ";"}}{ProcDecl ";" | FuncDecl ";" | ForwardDecl ";"}.Type = ...


5.3 Implementierung 113| PROCEDURE [FormalPars]| FUNCTION [FuncFormalPars]| ....ForwardDecl =(PROCEDURE "^" [Receiver] IdentDef [FormalPars])| (FUNCTION "^" [FuncReceiver] IdentDef [FuncFormalPars]).FuncDecl =FUNCTION [FuncReceiver] IdentDef [FuncFormalPars] ";"DeclSeq [BEGIN StatementSeq] END ident.FuncFormalPars ="(" [FuncFPSection {";" FuncFPSection}] ")" ":" Qualident.FuncFPSection =ident {"," ident} ":" Type.FuncReceiver ="(" ident ":" ident ")".Viele dieser Modifikationen würden uns erspart bleiben, wenn wir zusätzliche Regelnin die Sprachdefinition aufnehmen würden, <strong>der</strong>en Einhaltung dann durch semantischePrüfungen im Compiler sicherzustellen wäre. Es ist aber insgesamt gesehen einfacher,solche Prüfungen — wo immer dies möglich ist — schon durch die Syntax vorwegnehmenzu lassen.Wir müssen aber dennoch zwei Regel aufstellen, die garantieren, daß Funktionenkeine Seiteneffekte erzeugen können:• Innerhalb einer Funktion dürfen keine schreibenden Zugriffe auf Variablen stattfinden,die außerhalb <strong>der</strong> Funktion deklariert wurden.• Innerhalb einer Funktion dürfen nur an<strong>der</strong>e Funktionen, nicht aber an<strong>der</strong>e Prozedureno<strong>der</strong> Funktionsprozeduren aufgerufen werden.Die Einhaltung dieser Regeln muß selbstverständlich durch den Compiler geprüft werden.Damit sind die Spracherweiterungen für seiteneffektfreie Funktionen abgeschlossen.5.3 Implementierung5.3.1 Allgemeine Än<strong>der</strong>ungen in Scanner und ParserDie allgemeinen Än<strong>der</strong>ungen erstrecken sich (wie schon in Abschnitt 4.4.1) auf die Integration<strong>der</strong> neuen syntaktischen Elemente. Wir bringen also das neue SchlüsselwortFUNCTION in den Scanner OPS.Mod ein und modifizieren dann die Prozeduren Type(),FormalParameters() und Block() des Parsers OPP.Mod in geeigneter Weise. Alle <strong>weiteren</strong>Än<strong>der</strong>ungen sind spezifischer Natur und werden in den folgenden Abschnittenbeschrieben.


114 Prozeduren, Funktionen und Parameterübergabe5.3.2 Konstante ProzedurparameterUm die in Abschnitt 5.2.1 erarbeiteten Erweiterungen für konstante (schreibgeschützte)Parameter in OP2 einzubringen, müssen wir zunächst zwei neue Arten“ von Einträgen”in <strong>der</strong> Symboltabelle (Typ ObjDesc, siehe Abschnitt 3.3.1) bzw. in dem Syntaxbaum(Typ NodeDesc, siehe Abschnitt 3.3.1) definieren. Die Deklaration konstanter Parameterwird durch Instanzen vom Typ ObjectDesc beschrieben, <strong>der</strong>en Feld mode den WertConPar hat. Die Verwendung konstanter Parameter wird durch Instanzen vom TypNodeDesc beschrieben, <strong>der</strong>en Feld class den Wert Nconpar hat. Außerdem müssenexportierte Prozeduren, die konstante Parameter erwarteten, in <strong>der</strong> Symboldatei entsprechendgekennzeichnet werden. Dazu führen wir einen neuen tag tagConPar (sieheAbschnitt 3.3.4) ein. Die drei neuen Konstanten ConPar, Nconpar und tagConPar deklarierenwir zentral im Modul OPT.Mod und importieren sie — bei Bedarf — in an<strong>der</strong>eModule des Compilers.Im Parser OPP.Mod än<strong>der</strong>n wir dann die beiden Prozeduren FormalParameters()und Receiver() so ab, daß die Kennzeichung CONST zur Erzeugung eines Eintrags in<strong>der</strong> Symboltabelle führt, für den mode = ConPar gilt. In Receiver() implementierenwir auch die Prüfung, ob ein Empfängerparameter, <strong>der</strong> durch CONST als konstant gekennzeichnetwurde, wirklich einen Recordtyp hat.Im Modul OPT.Mod fügen wir in die Prozeduren Import() und OutPars() entsprechendeAnweisungen ein, damit konstante Parameter korrekt in den Symboldateien verwaltetwerden können. Außerdem entwickeln wir die Funktionsprozedur Structured(),die den Wert TRUE liefert, wenn <strong>der</strong> übergebene Typ im Sinne <strong>der</strong> konstanten Parameterals strukturiert“ anzusehen ist. 11”Die Prozedur NewLeaf() im Modul OPB.Mod muß so modifiziert werden, daß sie fürkonstante Parameter einen Knoten, für den class = Nconpar gilt, erzeugt. Außerdemsetzen wir für solche Knoten auch das Feld readonly auf TRUE. Sokönnen wir die in OP2schon vorhandenen Mechanismen für den schreibgeschützten Export wie<strong>der</strong>verwenden,um unsere konstanten Parameter zu implementieren. In <strong>der</strong> Prozedur Param() tragenwir konstante Parameter ebenfalls ein. Sie entscheidet, ob ein aktueller und ein formalerParameter kompatibel sind. Hier müssen wir zum ersten Mal unterscheiden, ob es sichbei dem Typ des konstanten Parameters um einen strukturierten o<strong>der</strong> einen elementarenTyp handelt. Für elementare Typen verwenden wir ja intern einen Wertparameter,damit als aktuelle Parameter Ausdrücke übergeben werden dürfen.Diese Unterscheidung setzt sich bei den folgenden Modifikationen im Backend vonOP2 fort. Wir führen sie dort im Modul OPV.Mod zunächst in <strong>der</strong> Prozedur ParamAdr()durch, in <strong>der</strong> unter an<strong>der</strong>em für jeden Parameter <strong>der</strong> entsprechende Platz auf dem Stackberechnet wird. In den Prozeduren AllocParams() und AssignParams(), diefür dieSpeicherbelegung und Initialisierung <strong>der</strong> Parameter auf dem Stack zuständig sind, führenwir sie ebenfalls durch. In <strong>der</strong> Prozedur Designator() müssen wir schließlich nur dieneue Konstante Nconpar einfügen. Auch im Modul OPC.Mod fügen wir entsprechendePrüfungen für die konstanten Parameter ein, und zwar in den Prozeduren MakeVar(),MakeTag() und MakeProc().11 Insbeson<strong>der</strong>e sind in diesem Sinn auch Zeichenketten strukturiert“, die normalerweise von OP2”als elementar“ angesehen werden.”


5.3 Implementierung 115Schließlich machen wir auch in dem Modul OPL.Mod noch eine Än<strong>der</strong>ung, und zwarin <strong>der</strong> Prozedur OutRefs(). Durch sie werden gewisse symbolische Informationen (zumBeispiel über die Parameter einer Prozedur) in die Objektdatei eingetragen (siehe auchAbschnitt 3.4.4 und Abschnitt 3.4.5). Diese Informationen werden durch das Oberon-System genutzt, um bei einem Laufzeitfehler einen aussagekräftigen stack-dump zu erzeugen,aus dem die Reihenfolge <strong>der</strong> Aufrufe sowie die Belegung <strong>der</strong> Variablen zu erkennenist, die zu diesem Laufzeitfehler geführt haben. Da wir am Oberon-System selbstkeine Än<strong>der</strong>ungen vornehmen wollen, behandeln wir konstante Parameter <strong>der</strong> Einfachheithalber wie Referenz- bzw. Wertparameter des entsprechenden Typs.Weitere Än<strong>der</strong>ungen an OP2 sind nicht nötig, die Implementierung <strong>der</strong> konstantenProzedurparameter ist damit also abgeschlossen.5.3.3 Seiteneffektfreie FunktionenIn diesem Abschnitt wollen wir die in Abschnitt 5.2.2 eingeführten Erweiterungen fürseiteneffektfreie Funktionen in den Compiler OP2 integrieren.Zunächst müssen wir uns entscheiden, wie Funktionen intern in OP2 repräsentiertwerden sollen. Da wir sowohl Funktionen als auch Funktionstypen eingeführt haben,müssen wir für den Typ OPT.StrDesc wie auch für OPT.ObjDesc neue Unterscheidungeneinführen. Funktionstypen werden durch Instanzen von StrDesc repräsentiert, fürdie form = FuncTyp gilt. Funktionen selbst werden durch Instanzen von ObjDesc repräsentiert,für die mode = LFunc (für private o<strong>der</strong> lokale Funktionen) bzw. mode =XFunc (für exportierte Funktionen) bzw. mode = TFunc (für typgebundene Funktionen)gilt. 12 Da alle semantischen Prüfungen für Funktionen im Frontend gemacht werdenkönnen, ist es aber nicht notwendig, die Knoten des Syntaxbaums (Typ OPT.NodeDesc)in ähnlicher Weise zu er<strong>weiteren</strong>. Hier können wir Funktionen bzw. Aufrufe von Funktionenauf Knoten abbilden, für die (wie auch für Funktionsprozeduren in Oberon-2) class= Nproc bzw. class = Ncall gilt. Die neuen Konstanten FuncTyp, LFunc, XFunc undTFunc deklarieren wir zentral im Modul OPT.Mod.Im Modul OPP.Mod entwickeln wir zunächst die Prozedur FuncFormalParameters(),die für Funktionen die Verarbeitung <strong>der</strong> Parameterliste übernimmt. Sie unterscheidetsich von <strong>der</strong> vorhandenen Prozedur FormalParameters() lediglich darin, daß sie keineVAR-Parameter erlaubt und die Angabe eines Ergebnistyps erzwingt. Auf die gleicheWeise definieren wir auch die Prozedur FuncReceiver(), die im Fall von typgebundenenFunktionen statt <strong>der</strong> vorhandenen Prozedur Receiver() verwendet wird.Die Verarbeitung <strong>der</strong> Deklarationen von Funktion und Funktionstypen erfor<strong>der</strong>t aberweitere Än<strong>der</strong>ungen. Wir entwickeln zunächst die Prozedur FunctionDeclaration(),die für Funktionen die Aufgabe <strong>der</strong> vorhandenen Prozedur ProcedureDeclaration()übernimmt, also (typgebundene) Funktionen in die Symboltabelle einträgt und entsprechendeKnoten des Syntaxbaums erzeugt. Hier müssen wir außerdem für typgebundenebzw. vorwärtsdeklarierte Funktionen Prüfungen einfügen, die sicherstellen, daß sie nicht12 Auf eine Umsetzung <strong>der</strong> Modi CProc bzw. IProc, die in OP2 verwendet werden, um code bzw.interrupt Prozeduren zu kennzeichnen, verzichten wir.


116 Prozeduren, Funktionen und Parameterübergabedurch Funktionsprozeduren redefiniert bzw. implementiert werden können. Wir müssenauch komplementäre Prüfungen in ProcedureDeclaration() nachtragen. Schließlichsind noch einige Än<strong>der</strong>ungen in den Prozeduren TypeDecl() und Block() notwendig,damit entsprechende Deklarationen erlaubt sind. In <strong>der</strong> Prozedur selector() müssenwir außerdem durch Einfügen einer <strong>weiteren</strong> Fallunterscheidung dafür sorgen, daß typgebundeneFunktionen aufgerufen werden können.Einige <strong>der</strong> <strong>weiteren</strong> Prüfungen, die für Funktionen notwendig sind, können wir ebenfallsdirekt in den Parser integrieren. Zunächst müssen wir aber eine Möglichkeit schaffen,mit<strong>der</strong>wirfeststellenkönnen,ob <strong>der</strong> Parser gerade eine Funktion übersetzt. Dazuführen wir im Modul OPP.Mod die globale Variable funcLevel: INTEGER ein, <strong>der</strong>en Wertwir innerhalb <strong>der</strong> Prozedur Block() vor dem Aufruf von FunctionDeclaration() umeins erhöhen und nach dem Aufruf um eins vermin<strong>der</strong>n. 13 Da Funktionen keine Prozedureno<strong>der</strong> Funktionsprozeduren aufrufen dürfen, macht es auch keinen Sinn, solche lokalzu einer Funktion zu deklarieren. Wir verbieten ihre Deklaration deshalb durch eineentsprechende Prüfung in Block(). Um Aufrufe von Prozeduren und Funktionsprozedurenallgemein zu verbieten, müssen wir auch in StatSeq() und Factor() Prüfungendurchführen:• StatSeq(): Aufrufe von Prozeduren sind nur dann erlaubt, wenn wir uns geradeaußerhalb einer Funktionsdeklaration befinden.• Factor(): Aufrufe von Funktionsprozeduren sind nur dann erlaubt, wenn wir unsgerade außerhalb einer Funktionsdeklaration befinden.Für Zuweisungen müssen wir zwischen lokalen und nicht-lokalen Zielen unterscheiden:An lokale Variablen dürfen auch in Funktionen Zuweisungen erfolgen, an nicht-lokaleVariablen hingegen nicht. In diesem Zusammenhang stellen Variablen eines Zeigertypseinen Son<strong>der</strong>fall dar: Zuweisungen an lokale Zeigervariablen dürfen wir zwar erlauben,nicht aber Zuweisungen an die — durch einen lokalen Zeiger — referenzierte anonymeVariable, da diese ja nicht-lokal zur Funktion sein könnte. 14 Die entsprechende Prüfungintegrieren wir wie<strong>der</strong> in die Prozedur StatSeq(): Wir entwickeln eine lokale ProzedurCheckAssignDest(), die vor je<strong>der</strong> Zuweisung aufgerufen wird und die das Ziel <strong>der</strong> Zuweisungnach unseren Kriterien überprüft. Eine ähnliche Prüfung muß aber auch füralle vordeklarierten Prozeduren gemacht werden, die übergebenen Variablen einen neuenWert zuweisen. Im einzelnen sind dies die Prozeduren NEW(), INC(), DEC(), INCL(),EXCL() und COPY(). 15 Für die Prüfung <strong>der</strong> an diese Prozeduren übergebenen Parameterentwickeln wir eine neue Prozedur CheckDestPar(), die wir lokal zu <strong>der</strong> ProzedurStandProcCall() deklarieren und dort auch entsprechend aufrufen.13 Wir können hier nicht mit einer Variable vom Typ BOOLEAN arbeiten, da Funktionen ja geschachteltdeklariert werden können.14 Dies ist genau <strong>der</strong> (in Abschnitt 5.1.2 angesprochene) Unterschied zwischen R-Funktionen und S-Funktionen. In<strong>Fro<strong>der</strong>on</strong>-1 beschränken wir uns auf R-Funktionen und verbieten deswegen Zuweisungenan durch Zeiger referenzierte Variablen.15 Eigentlich sollten auch einige Prozeduren aus dem Pseudo-Modul SYSTEM entsprechend geprüftwerden. Da es sich hierbei jedoch um Operationen auf niedrigster Ebene handelt, gehen wir davon aus,daß <strong>der</strong> Programmierer sich im klaren über die Konsequenzen ihrer Verwendung ist. Deswegen prüfenwir die Parameter dieser Prozeduren nicht.


5.3 Implementierung 117Wenden wir uns nun dem Modul OPB.Mod zu. Dort müssen wir einige <strong>der</strong> vorhandenensemantischen Prüfungen für Funktionen entsprechend erweitern. Als erstes fügenwir eine weitere Bedingung in die Prozedur CheckParameters() ein: Wenn ein formalerParameter einen Funktionstyp hat, dann muß auch <strong>der</strong> entsprechende aktuelle Parametereinen Funktionstyp — und nicht etwa den Typ einer Funktionsprozedur — haben.In <strong>der</strong> Prozedur CheckProc() müssen wir für Funktionen einen neuen Fall einführen,<strong>der</strong> darüber entscheidet, ob eine Funktionsvariable und eine Funktion einan<strong>der</strong> zugewiesenwerden dürfen. Eine ähnliche Modifikation müssen wir auch in <strong>der</strong> ProzedurCheckAssign() vornehmen: An eine Funktionsvariable dürfen nur Funktionen zugewiesenwerden, nicht aber Funktionsprozeduren. Die umgekehrte Zuweisung einer Funktionan eine Prozedurvariable könnten wir zwar zulassen, aber damit <strong>der</strong> Compiler darauseinen Vorteil ziehen kann, müßten wir dann die Prozedurvariable entsprechend kennzeichnen.Auf Optimierungsfragen (und nur dafür wäre diese Information von Nutzen)gehen wir aber im Rahmen dieser Arbeit nicht ein. Deswegen verbieten wir eine solcheZuweisung vorerst, halten uns aber für spätere Versionen des Compilers die Möglichkeitoffen, sie zu erlauben. Eine weitere Än<strong>der</strong>ung ist in den Prozeduren ConstCmp(),ConstOp() und Op() nötig, da Funktionsvariablen (wie Prozedurvariablen) mit demWert NIL vergleichbar sein sollen. Wir fügen also den entsprechenden CASE-Labels auchden Fall FuncTyp hinzu. Da das Modul OPB.Mod auch die Knoten des Syntaxbaumserzeugt, müssen wir außerdem die Prozedur NewLeaf() so erweitern, daß für FunktionenKnoten mit class = Nproc erzeugt werden. Die Prozeduren Field(), PrepCall()und Call() müssen in gleicher Weise modifiziert werden, damit Funktionen überhauptaufgerufen werden können. Schließlich müssen wir auch in den Prozeduren StPar0(),und StPar1() Än<strong>der</strong>ungen vornehmen: Wir nehmen Funktionen als zulässige Parameterfür die vordeklarierten Prozeduren SIZE(), SYSTEM.GETREG(), SYSTEM.PUTREG(),SYSTEM.GET() und SYSTEM.PUT() auf.In den Modulen des Backends (OPV.Mod, OPC.Mod und OPL.Mod) fallen ebenfalls eineReihe von Än<strong>der</strong>ungen an, allerdings sind hier keine <strong>weiteren</strong> Prüfungen mehr notwendig.In den folgenden Prozeduren werden neue Fallunterscheidungen benötigt:• OPV.Mod: TypeSize(), Traverse(), Expression(), StatSeq(), ProcSize().• OPC.Mod: WriteStaticLink(), MakeProc(), Convert().• OPL.Mod: FindTProcs().Außerdem müssen wir auch die SET-Konstante LongSet in den Modulen OPC.Mod undOPL.Mod anpassen, da Funktionen (o<strong>der</strong> besser: Funktionsvariablen und -typen) dieGröße eines Langworts haben.Einzig und allein die Tatsache, daß Funktionen wie Prozeduren und Funktionsprozedurenexportiert werden können, haben wir bis jetzt außer acht gelassen. Wir müssenalso noch entsprechende Erweiterungen in den Symboldateien vornehmen. Dazu führenwir zunächst im Modul OPT.Mod neue tags ein, durch die wir Funktionen und typgebundeneFunktionen in den Symboldateien kennzeichnen können. Dabei ist allerdingsdarauf zu achten, daß in OP2 durch tags nicht nur die Art eines Objekts gekennzeichnetwird, son<strong>der</strong>n auch sein Exportstatus. Deswegensindfür Funktionstypen die neuen


118 Prozeduren, Funktionen und Parameterübergabetags tagFunc und tagHdFunc, dieöffentliche Funktionstypen und private Fel<strong>der</strong> voneinem Funktionstyp innerhalb von Recordtypen darstellen, notwendig. 16 ExportierteFunktionen selbst werden durch tagXFun gekennzeichnet. Für typgebundene Funktionenbenötigen wir die tags tagTFun und tagHdTFun. Natürlich müssen wir auch dieProzeduren Import(), OutHdFld() und OutObj() entsprechend erweitern, so daß dieneuen tags korrekt erkannt und umgesetzt werden. Damit ist die Implementierung <strong>der</strong>seiteneffektfreien Funktionen abgeschlossen.16 Auch an<strong>der</strong>e nicht exportierte Fel<strong>der</strong> (von Zeigertypen und Prozedurtypen) werden teilweise mit indie Symboldatei aufgenommen. Der Grund dafür sind einige Implementierungen des Oberon-Systems,die diese Informationen nutzen können.


Kapitel 6Klassen, Typen und Objektorientierung“Object-oriented” is the latest in term, complementing orperhaps even replacing “structured” as the high-tech versionof “good”.— Bertrand Meyer, in [43]Es herrscht zur Zeit eine gewisse Euphorie bezüglich <strong>der</strong>objektorientierten Programmierung. ...Diese Euphoriewird sich legen. ...Man wird ...Klassen ganz selbstverständlichverwenden und als das sehen, was sie sind:Bausteine, die helfen, modulare und erweiterbare Softwarezu entwickeln.— Hanspeter Mössenböck, in [47]In diesem Kapitel beschäftigen wir uns näher mit den Konzepten objektorientierter Programmiersprachenund <strong>der</strong>en Umsetzung in Oberon-2. Zunächst gehen wir auf Schwachpunktedes Objektmodells von Oberon-2 ein und schlagen ein neues Modell vor, das sichvor allem durch eine Erhöhung <strong>der</strong> Flexibilität von Oberon-2-Programmen auszeichnet.In den anschließenden Abschnitten beschäftigen wir uns mit entsprechenden Spracherweiterungenfür Oberon-2 und zeigen, wie diese in den Compiler OP2 integriert werdenkönnen.


120 Klassen, Typen und Objektorientierung6.1 Einführung und MotivationIn diesem Abschnitt stellen wir eine Reihe von Problemen im Zusammenhang mit objektorientierterProgrammierung dar, die sich in Oberon-2 nicht direkt lösen lassen. Vieledieser Probleme motivieren Spracherweiterungen, die wir in Abschnitt 6.2 für <strong>Fro<strong>der</strong>on</strong>-1entwerfen und in Abschnitt 6.3 (soweit möglich) in den Compilers OP2 einbringen.Zunächst wollen wir hier aber in Anlehnung an [9] eine Notation einführen, die sichim folgenden als nützlich erweist. Sie betrifft die Relation <strong>der</strong> Typerweiterung (sieheAbschnitt 2.4.1). Im folgenden stehen T0, T1, T2 und Tn für in Oberon-2 deklarierteRecordtypen. Wenn T1 direkt von T0 abgeleitet wurde o<strong>der</strong> umgekehrt T0 <strong>der</strong> direkteBasistyp von T1 ist, dann schreiben wir T1 ✄ T0. Die Relation ✄ ist transitiv (T1 ✄ T0∧ T2✄T1 ⇒ T2✄T0) reflexiv (T0✄T0) und antisymetrisch (T1✄T0 ∧ T0✄T1 ⇒ T0= T1) und stellt deswegen auch eine Teilordnung auf <strong>der</strong> Menge aller Recordtypen dar.Wenn Tn indirekt von T0 abgeleitet wurde o<strong>der</strong> umgekehrt T0 ein indirekter Basistypvon Tn ist, dann schreiben wir Tn ✄ ∗ T0, wobei durch ✄ ∗ die <strong>der</strong> Relation entsprechendetransitive Hülle angedeutet wird.6.1.1 Sichtbarkeit in KlassenKlassen stellen — ähnlich wie Module — Einheiten aus Daten und Operationen dar,die gewisse Details ihrer Implementierung vor an<strong>der</strong>en Klassen bzw. Modulen verbergen(Kapselung, Geheimnisprinzip, information hiding).Bezüglich <strong>der</strong> Sichtbarkeit von Bezeichnern unterscheiden wir für ein Modul A zwischendem Modul selbst (interne o<strong>der</strong> private Sichtbarkeit) und Klienten des Moduls(externe o<strong>der</strong> öffentliche Sichtbarkeit). Unter einem Klienten des Moduls A verstehenwir hier jedes an<strong>der</strong>e Modul X, dasA importiert. Da in Oberon-2 zwischen zweiModulen keine <strong>weiteren</strong> Beziehungen als die des Imports bestehen, reichen diese zweiUnterscheidungen bezüglich <strong>der</strong> Sichtbarkeit von Bezeichnern aus.Auch zwischen zwei Klassen kann natürlich eine Beziehung, die dem oben angesprochenenImport zwischen Modulen entspricht, bestehen: Eine Klasse X kann zurImplementierung ihrer Methoden auf eine Instanz einer Klasse A zurückgreifen und <strong>der</strong>enMethoden aufrufen. Auch hier sprechen wir davon, daß die Klasse X ein Klient <strong>der</strong>Klasse A ist, und auch hier reichen zwei Unterscheidungen bezüglich <strong>der</strong> Sichtbarkeitvon Bezeichnern aus.Allerdings kann zwischen zwei Klassen auch eine weitere Beziehung bestehen, die fürModule nicht existiert: Eine Klasse C1 kann von einer Klasse C0 abgeleitet werden,wobei C0 und C1 nicht im selben Modul deklariert sein müssen. Da eine abgeleiteteKlasse eine Spezialisierungen bzw. Erweiterungen ihrer Basisklasse darstellt, kann es —vom Standpunkt <strong>der</strong> Wie<strong>der</strong>verwendbarkeit aus betrachtet — sinnvoll sein, eine weitereUnterscheidung bezüglich <strong>der</strong> Sichtbarkeit von Bezeichnern durchzuführen: 1 GewisseBezeichner einer Klasse C0 sollten für abgeleitete Klassen C1...Cn sichtbar sein, fürKlienten jedoch nicht.1 Sowohl Spezialisierung als auch Erweiterung sind legitime Sichten auf das Konzept <strong>der</strong> Vererbung.Siehe auch [43].


6.1 Einführung und Motivation 121Da Oberon-2 aber nicht über einen solchen zusätzlichen Sichtbarkeitsbereich fürabgeleitete Klassen verfügt, müssen an<strong>der</strong>e Möglichkeiten verwendet werden, um Klassenerweiterbar zu halten. Betrachten wir zum Beispiel zwei Klassen aus einer graphischenAnwendung (ähnlich <strong>der</strong> in Abschnitt 2.6 vorgestellten), die Rechtecke undgefüllte Rechtecke repräsentieren sollen. Beide Klassen benötigen zur Implementierung<strong>der</strong> Methode Draw() die Höhe und Breite des Rechtecks. Um diese Informationen beidenKlassen zugänglich zu machen, bestehen eine Reihe von Möglichkeiten: 2• Die Klasse Rectangle exportiert Methoden, die Klienten (und damit auch abgeleitetenKlassen) den Zugriff auf die Ausmaße des Rechtecks ermöglichen (so sindwir in Abschnitt 2.6 vorgegangen).• Die Klasse FilledRectangle redefiniert die Methoden, mit denen die Ausmaße einesRechtecks festgelegt werden. Dadurch kann sie die Ausmaße nochmals lokalspeichern.• Die Klasse Rectangle exportiert direkt die Datenfel<strong>der</strong>, in denen die Ausmaße desRechtecks gespeichert werden.Jede dieser Möglichkeiten hat aber gewisse Nachteile: Im ersten Fall verlieren wir durchdie stets notwendigen Methodenaufrufe Laufzeiteffizienz (dieser Nachteil hat auch denschreibgeschützten Export von Oberon-2 motiviert, siehe Anmerkung 6.1). Im zweitenFall verlieren wir durch die redundante Speicherung <strong>der</strong> Ausmaße Speicherplatzeffizienzund führen außerdem eine neue potentielle Fehlerquelle ein, da die redundanten Ausmaßekonsistent gehalten werden müssen (es müßten auch alle an<strong>der</strong>en Methoden, die dieGröße eines Rechtecks verän<strong>der</strong>n können, redefiniert werden). Die dritte Möglichkeit istdagegen in je<strong>der</strong> Hinsicht effizient, aber durch das Exportieren <strong>der</strong> Datenfel<strong>der</strong> gibt eskeine Möglichkeit mehr, gewisse Invarianten für die Klasse zu garantieren. 3 Hinzu kommtaußerdem, daß bei <strong>der</strong> ersten und dritten Möglichkeit Klienten unter Umständen mitInformationen ”belastet“ werden, die sie gar nicht benötigen. 4Gerade die dritte Möglichkeit wird in Oberon-2 aber häufig genutzt, was zum Beispiel<strong>der</strong> von Josef Templ entwickelte Grafikeditor Kepler 5 verdeutlicht: In diesem Systembestehen Grafiken aus einer Anzahl von Sternen (Punkten), die zu einer Anzahl vonKonstellationen (geometrische Figuren bzw. Formen) zusammengesetzt werden können.Die Klasse Constellation exportiert öffentlich (also nicht schreibgeschützt!) Details<strong>der</strong> Implementierung — nämlich die zu einer Konstellation gehörenden Sterne — um2 Wenn wir im folgenden davon sprechen, daß eine Klasse gewisse Fel<strong>der</strong> o<strong>der</strong> Methoden exportiert,so meinen wir damit natürlich, daß sie in dem umgebenden Modul als exportiert gekennzeichnet sind.Eigentlich exportiert also das Modul und nicht die Klasse.3 Diese können ja auch ohne die Spracherweiterungen aus Kapitel 4 garantiert werden, wenn auchnicht explizit bzw. für einen Klienten sichtbar.4 Das Geheimnisprinzip (information hiding) will ja hauptsächlich die Klienten entlasten und nichtetwa ”aus Eigennutz“ wichtige Informationen vor ihnen zurückhalten.5 Dieser Grafikeditor ist in vielen Distributionen des Oberon-Systems enthalten. Mehr über Keplerfindet sich beispielsweise in [61] o<strong>der</strong> in <strong>der</strong> Datei Kepler.Text <strong>der</strong> jeweiligen Distribution.


122 Klassen, Typen und ObjektorientierungAnmerkung 6.1 Der schreibgeschützte (read-only) ExportinOberon-2Mit Oberon-2 wurde für Variablen und Fel<strong>der</strong> von Recordtypen die Möglichkeit eingeführt,sie durch die Marke -“alsschreibgeschützt zu exportierten. Der Hauptgrund”dafür war laut [53, Seite 277] die Möglichkeit, in bestimmten Fällen auf spezielle Zugriffsprozedurenverzichten zu können. Über solche müßte eine Variable sonst zugänglichgemacht werden, wenn gleichzeitig gewisse Invarianten garantiert werden sollen. Obwohldiese Möglichkeit pragmatisch sehr sinnvoll sein kann, ist es unbefriedigend, daß durch siedie Konzepte <strong>der</strong> Sichtbarkeit und <strong>der</strong> Verän<strong>der</strong>barkeit — im Sinne von Verwendbarkeit”als Ziel einer Zuweisung“ — vermischt werden. In Oberon-2 ist diese Vermischung nichtoffensichtlich, sie wird es aber, wenn wir einen <strong>weiteren</strong> Sichtbarkeitsbereich einführenwollen: Es könnte genauso sinnvoll sein, Bezeichner, die lediglich zwischen abgeleitetenKlassen sichtbar sind, zusätzlich auch als schreibgeschützt zu kennzeichnen. Dieentsprechenden Konzepte sollten orthogonal zueinan<strong>der</strong> sein.es abgeleiteten Klassen wie Rectangle, die in externen Modulen implementiert sind, zuermöglichen, auf diese Daten zuzugreifen.Diese Überlegungen lassen auch für Oberon-2 den Wunsch nach drei Arten vonSichtbarkeitsbereichen für Bezeichner in Klassen aufkommen: Einen internen Sichtbarkeitsbereich,<strong>der</strong> nur innerhalb einer Klasse gilt, einen externen, <strong>der</strong>für Klientengilt, und einen dritten, geschützten Sichtbarkeitsbereich, <strong>der</strong> für abgeleitete Klassen gilt.Natürlich sollte auch dieser Sichtbarkeitsbereich (wie die an<strong>der</strong>en) an das umgebendeModul gebunden sein, da sonst eine Grundregel“ des Entwurfs von Oberon-2 verletzt”würde.6.1.2 Abstrakte Klassen und MethodenBei <strong>der</strong> <strong>Entwicklung</strong> von objektorientierten Anwendungen tritt häufig <strong>der</strong> Wunsch auf,die Implementierung gewisser Klassen bzw. gewisser Methoden vollständig an abgeleiteteKlassen zu delegieren. In Abschnitt 2.6 deklarieren wir die Klasse Shape beispielsweiseso, daß sie lediglich die Signatur <strong>der</strong> Methode Draw() vorgibt, die unsere Anwendungvon allen geometrischen Formen erwartet. Wir wollen hier also lediglich die gemeinsameSchnittstelle für geometrische Formen definieren, nicht jedoch eine konkrete Formselbst. Wir bezeichnen Shape deswegen auch als abstrakte Klasse, dieMethodeDraw()dementsprechend als abstrakte Methode. 6Wie ebenfalls in Abschnitt 2.6 gezeigt, realisieren wir abstrakte Methoden in Oberon-2 durch konkrete Methoden, die einen Aufruf <strong>der</strong> vordeklarierten Prozedur HALT() enthalten.Auf diese Weise wird unser Programm mit einem Laufzeitfehler abgebrochen,falls wir vergessen sollten, die entsprechende, abstrakte Methode in einer konkretenKlasse zu redefinieren (bzw. sie überhaupt erstmals ”richtig“ zu implementieren).Damit riskieren wir aber einen Laufzeitfehler für eine Prüfung, die grundsätzlichschon zum Zeitpunkt <strong>der</strong> Übersetzung möglich wäre (siehe auch [61, Abschnitt 7.3]):6 Die in [21] vorgestellten Entwurfsmuster Abstract Factory bzw. Factory Method sind weitereBeispiele für die Verwendung abstrakter Klassen und Methoden.


6.1 Einführung und Motivation 123Bestünde die Möglichkeit, abstrakte Methoden in Oberon-2 explizit zu kennzeichnen,so könnte <strong>der</strong> Compiler sicherstellen, daß jede abstrakte Methode in einer konkretenKlasse redefiniert (bzw. implementiert) wird. Darüber hinaus könnten wir auch eineweitere mögliche Fehlerquelle ausschließen: Schon <strong>der</strong> Compiler würde verhin<strong>der</strong>n, daßüberhaupt Variablen abstrakter Klassen angelegt werden können (sei es nun durch einedirekte Deklaration o<strong>der</strong> durch einen Aufruf von NEW()).Folglich sollte auch Oberon-2 um Konstrukte erweitert werden, die eine explizite Deklarationabstrakter Klassen sowie damit verbundene statische Prüfungen ermöglichen.6.1.3 Subtyping und SubclassingDurch die Vererbungs- bzw. Typerweiterungs-Relation zwischen Klassen werden genaugenommeneigentlich zwei verschiedene Relationen induziert:• Subtyping: Die durch einen abgeleiteten Typ repräsentierten Werte sind eine Teilmenge<strong>der</strong> durch seinen Basistyp repräsentierten Werte. Deswegen dürfen Variablendes abgeleiteten Typs überall dort verwendet werden, wo eine Variable desBasistyps erwartet wird.• Subclassing: Die Datenfel<strong>der</strong> und Methoden <strong>der</strong> Basisklasse werden an die abgeleiteteKlasse vererbt 7 und können zur Implementierung <strong>der</strong> abgeleiteten Klassewie<strong>der</strong>verwendet werden.Es ist klar, daß Polymorphismus und dynamisches Binden (von Aufrufen an Methoden)lediglich auf die Subtyping-Relation angewiesen sind. Weiterhin sehen wir, daß fürdiese Anwendungen die Kenntnis <strong>der</strong> Schnittstelle einer Klasse ausreichend ist. Wirbezeichnen im folgenden die Schnittstelle einer Klasse als ihre Signatur. 8Die Wie<strong>der</strong>verwendung einer schon vorhandenen Implementierungen (im Sinne vonAufrufen an redefinierte Methoden bzw. Nutzung gewisser Datenfel<strong>der</strong> <strong>der</strong> Basisklasse)benötigt hingegen die Subclassing-Relation. 9 In diesem Sinne sind Klassen also lediglichImplementierungen von Signaturen, die gegebenenfalls auf die Implementierung an<strong>der</strong>erKlassen zurückgreifen.Die Vermischung dieser beiden Relationen in Oberon-2 (aber auch in an<strong>der</strong>en Programmiersprachenwie Eiffel und C++) hat negative Auswirkungen auf den gesamtenEntwurfsprozeß für objektorientierte Software-Systeme. Betrachten wir als Beispiel wie<strong>der</strong>umeine graphische Anwendung ähnlich <strong>der</strong> in Abschnitt 2.6 vorgestellten. DieseAnwendung soll (neben an<strong>der</strong>en Formen) auch über Rechtecke und Quadrate verfügen.In welcher Vererbungsrelation sollten hier die entsprechenden Klassen Shape, Rectangle7 Sie werden sozusagen automatisch kopiert“, auch wenn Implementierungen dieser Art für Vererbungselten sind.8 Dabei definieren wir die Signatur einer Klasse über die Menge <strong>der</strong> Signaturen aller in ihr deklarierten”Methoden. Alternativ sprechen wir auch vom Typ <strong>der</strong> Klasse, und zwar im Sinne eines abstraktenDatentyps (siehe auch Abschnitt 4.2.6).9 Es wäre aber auch denkbar, diese Wie<strong>der</strong>verwendung <strong>der</strong> Implementierung über an<strong>der</strong>e Konstrukteals Subclassing zu lösen, zum Beispiel durch Delegation (siehe auch [59, S. 208f]).


124 Klassen, Typen und Objektorientierungund Square bei einer Implementierung in Oberon-2 stehen? Untersuchen wir drei verschiedeneLösungsvorschläge:• Sowohl Rechtecke als auch Quadrate sind geometrische Formen.Relationen sind also Rectangle ✄ Shape und Square ✄ Shape.Die gesuchten• Rechtecke sind spezielle geometrisch Formen. Quadrate sind spezielle Rechtecke.Die gesuchten Relation ist also Square ✄ Rectangle ✄ Shape.• <strong>Zur</strong> Beschreibung eines Quadrats ist lediglich eine Seitenlänge erfor<strong>der</strong>lich, zur Beschreibungeines Rechtecks aber zwei. Die gesuchte Relation ist also Rectangle ✄Square ✄ Shape, wobeidiezweiteSeitenlänge erst in Rectangle eingeführt wird.Der dritte Lösungsvorschlag ist offensichtlich falsch. Hier wurde lediglich die Perspektivedes Subclassing (also die Erweiterung <strong>der</strong> Implementierung) berücksichtigt, nicht aberdie des Subtyping (und <strong>der</strong> damit verbundenen Substituierbarkeit): Ein Rechteck kannnicht überall dort verwendet werden, wo ein Quadrat erwartet wird! Die beiden erstenLösungen sind zulässig, unterscheiden sich aber stark voneinan<strong>der</strong>:• In <strong>der</strong> ersten Lösung sind Quadrate nicht von Rechtecken abgeleitet, sie könnenalso nicht an Stelle von Rechtecken verwendet werden. Damit ist diese Lösung nurbegrenzt einsetzbar.• In <strong>der</strong> zweiten Lösung muß für Quadrate entwe<strong>der</strong> eines <strong>der</strong> Fel<strong>der</strong>, die für Rechteckeschon vorhanden sind, wie<strong>der</strong>verwendet werden, o<strong>der</strong> beide Fel<strong>der</strong> müssenkonsistent gehalten werden, o<strong>der</strong> es muß ein neues Feld für die Seitenlänge desQuadrats eingeführt werden.Dagegen würde sich bei einer Trennung von Subtyping und Subclassing die folgendeLösung anbieten: 10• Die Signaturen Shape, Rectangle und Square stehen in <strong>der</strong> Beziehung Square ✄Rectangle ✄ Shape, womit die Substituierbarkeit sichergestellt ist.• Die Klassen ShapeImp, RectImp und SquareImp implementieren die jeweilige Signaturen.Sie stehen untereinan<strong>der</strong> in den Beziehungen RectImp ✄ ShapeImp undSquareImp ✄ ShapeImp, um Teile <strong>der</strong> Implementierung von allgemeinen geometrischenFormen wie<strong>der</strong>zuverwenden.• Alle Module <strong>der</strong> Anwendung (bis auf eine Ausnahme) arbeiten nur mit Signaturen.Genau ein Modul (bzw. eine Klasse), das für die Erzeugung von neuen Instanzenzuständig ist, arbeitet mit den konkreten Klassen (bzw. Implementierungen).10 Wir verwenden im folgenden die am Anfang dieses Kapitels für die Typerweiterung eingeführteNotation T1 ✄ T0 auch für die separaten Relationen Subtyping und Subclassing. Ihre Bedeutung sollteauch für diese Relationen klar sein.


6.1 Einführung und Motivation 125Offensichtlich trägt eine Trennung <strong>der</strong> beiden Relationen zur Erhöhung <strong>der</strong> Flexibilitätbei. Nachteilig ist auf den ersten Blick lediglich, daß nunmehr zwei Hierarchien vonTypen (im Sinne <strong>der</strong> Programmiersprache) verwaltet werden müssen.Auch John Porter argumentiert in [51] für eine Trennung <strong>der</strong> üblichen“ Vererbungs-Relationin die zwei hier dargestellten Subtyping- und Subclassing-Relationen. Er”kommt dort für die Programmiersprache Portlandish zu folgendem Ergebnis:...the Portlandish language separates the notion of class into specificationand implementation. A specification is given by a type, which is little morethan a set of message signatures. An implementation provides a representationfor the state of instances and a set of method bodies to implementthose messages. To complete the separation between specification and implementationit is necessary to separate the inheritance hierarchy into twohierarchies. In Portlandish, types are related by a subtype relationship andimplementations are related by a code-sharing form of inheritance. ...As aresult of this study it is our opinion that (1) two hierarchies can be supportedwithout unduly complicating a language’s definition and (2) enough gain inexpressibility is achieved that two hierarchies should be supported.Aber nicht nur exotische“ Programmiersprachen wie Portlandish gehen inzwischen nach”diesem Prinzip vor, es findet sich zum Beispiel auch in <strong>der</strong> Programmiersprache JAVAin Form von interfaces (siehe etwa [50, S. 148ff]).Auch in Programmiersprachen, die diese Trennung nicht durchführen, wird sie oft alsEntwurfsmuster“ angewandt: Signaturen in unserem Sinne werden durch vollständig”abstrakte Klassen (siehe Abschnitt 6.1.2) simuliert. Problematisch ist hier allerdings,das simulierte Signaturen“ in <strong>der</strong> gleichen Hierarchie wie Klassen vorkommen und deswegenzum Beispiel unser Problem mit den Rechtecken und Quadraten nicht in <strong>der</strong> oben”dargestellten Form lösbar ist. 11Zusammenfassend wäre es also auch in Oberon-2 durchaus wünschenswert, eine Trennungvon Spezifikation und Implementierung von Klassen durchzuführen, sogar wenn wirdann zwei Hierarchien von Typen verwalten müssen.6.1.4 Einfach- und MehrfachvererbungDie Programmiersprache Oberon-2 erlaubt nur die Konstruktion von Klassenhierarchien,die graphentheoretisch gesehen Bäume (trees) sind: 12 Jede einzelne Klasse ist entwe<strong>der</strong>von keiner o<strong>der</strong> von genau einer an<strong>der</strong>en Klasse abgeleitet. Programmiersprachen wieEiffel o<strong>der</strong> C++ hingegen erlauben es auch, eine Klasse von mehreren an<strong>der</strong>en Klassenabzuleiten. Hier sind Klassenhierarchien (wie<strong>der</strong>um graphentheoretisch gesehen) gerichtete,azyklische Graphen (directed acyclic graphs, DAGs). Programmiersprachen wie11 In diesen Sprachen fehlt ein Konstrukt, um einer simulierten Signatur“ ohne die Verwendung <strong>der</strong>”gewöhnlichen“ Vererbung eine Implementierung zuzuordnen.” 12 In Oberon-2 ist die Klassenhierarchie eher ein Wald (forest), da nicht jede Klasse von mindestenseiner an<strong>der</strong>en abgeleitet sein muß; es gibt in Oberon-2 keine universelle Basisklasse“ wie zum Beispiel”Object in SmallTalk o<strong>der</strong> ANY in Eiffel [45].


126 Klassen, Typen und ObjektorientierungOberon-2 unterstützen also Einfachvererbung, solchewieEiffel o<strong>der</strong> C++ unterstützenauch Mehrfachvererbung. Die Integration von Mehrfachvererbung in eine Programmiersprachezieht aber eine Reihe von Problemen nach sich (siehe auch [46, S. 113f]):• Es kann zu nachträglichen Namenskonflikten zwischen Datenfel<strong>der</strong>n bzw. Methodeneinzelner Klassen kommen, wenn diese gleichzeitig als Basisklassen verwendetwerden.• Wird eine Klasse von zwei (o<strong>der</strong> mehreren) Klassen abgeleitet, die wie<strong>der</strong>um alleeine gemeinsame Basisklasse haben, dann tritt die Frage auf, ob Datenfel<strong>der</strong> undMethoden ”doppelt“ geerbt werden sollen.• Klassenbibliotheken, die Mehrfachvererbung nutzen, sind durch ihre allgemeinereStruktur (DAG im Gegensatz zu Baum o<strong>der</strong> Wald) schwerer zu verstehen undschwerer zu warten.• Mehrfachvererbung führt zu höheren Kosten, was Methodenaufrufe angeht, dakompliziertere ”Suchvorgänge“ notwendig sind.Hinzu kommt außerdem, daß Mehrfachvererbung schwerer formal zu behandeln ist alsEinfachvererbung. 13 Es existieren aber trotzdem Probleme zu <strong>der</strong>en Lösung die Verwendungvon Mehrfachvererbung aus pragmatischer Sicht sinnvoll ist.Eine interessante Anwendung für Mehrfachvererbung sind sogenannte Wesenszüge(traits) von Klassen (siehe auch [7] für eine frühe Darstellung). Ein Beispiel für einenWesenszug wäre etwa Printable: Dieser Wesenszug würde beschreiben, welche Methodeneine Klasse implementieren muß, um in einem bestimmten Kontext (zum Beispielim Rahmen eines Druckerspoolers) als druckbar“ zu gelten. Wesenszüge sind in diesem”Sinne also Signaturen. Konkrete Klassen müssen die Methoden <strong>der</strong> jeweiligen Signaturimplementieren, um den durch sie repräsentierten Wesenszug zu tragen“. Neben Printablekönnten natürlich auch weitere Wesenszüge definiert sein, zum Beispiel Mailable,”<strong>der</strong> Objekte als per email verschickbar“ kennzeichnet, o<strong>der</strong> Storable bzw. Persistent,”<strong>der</strong> Objekte als auf ein externes Medium speicherbar“ kennzeichnet. Wesenszüge dienen”meist zur Repräsentation sehr allgemeiner Eigenschaften, sie können also für verschiedeneKlassen aus verschiedenen Anwendungen passend bzw. nützlich sein. Es machtdeshalb Sinn, sie nur ein einziges Mal zu definieren und es den einzelnen Klassen zuüberlassen, anzugeben, welche Wesenszüge sie tragen wollen.In vielen objektorientierten Programmiersprachen wird zur Implementierung von WesenszügenMehrfachvererbung eingesetzt. Für jeden Wesenszug wird eine (abstrakte)Klasse definiert, von <strong>der</strong> an<strong>der</strong>e Klassen, die diesen Wesenszug tragen sollen, abgeleitetwerden. Der Einsatz von Mehrfachvererbung ist in diesem Fall vor allem deswegensinnvoll, da sich bei Einfachvererbung alle Wesenszüge in einer universellen Basisklasseansammeln würden. Dadurch würden aber alle Klassen stets alle Wesenszüge tragen,13 In [42] wird das Problem für die Programmiersprache BETA wie folgt beschrieben: ”BETA doesnot have multiple inheritance, due to the lack of a profound theoretical un<strong>der</strong>standing, and also becausethe current proposals seem technically very complicated.“.


6.1 Einführung und Motivation 127und Klassen, für die ein gewisser Wesenszug keinen Sinn macht, müßten die entsprechendenMethoden so überschreiben, daß ihr Aufruf zu einem Laufzeitfehler führt. 14Betrachten wir ein konkretes Beispiel für die bei <strong>der</strong> Implementierung von Wesenszügenentstehenden Probleme in Oberon-2. Wir wollen einen Wesenszug Persistenteinführen, <strong>der</strong> die ”Speicherbarkeit“ eines Objekts auf eine Datei ausdrückt. Dazu verwendenwir zwei Klassen Persistent (siehe Listing 6.1) und File (siehe Listing 6.2):Die Klasse Persistent stellt den Wesenszug dar und definiert zwei Methoden StoreOn()und RestoreFrom(). Sie ist vollständig abstrakt, da ja erst in konkreten Klassen bekanntist, welche Datenfel<strong>der</strong> in welcher Form gespeichert werden müssen. Erst in konkretenKlassen, die ”Persistent“ sein sollen, werden die Methoden dann geeignet überschrieben.Listing 6.1 Ein Wesenszug für persistente Objekte (Oberon-2)TYPE Persistent = POINTER TO PersistentDesc;PersistentDesc = RECORD END;PROCEDURE (self: Persistent) StoreOn (file: File);(* store content of this object *)BEGIN HALT(99);END StoreOn;PROCEDURE (self: Persistent) RestoreFrom (file: File);(* restore content of this object *)BEGIN HALT(99);END RestoreFrom;Die konkrete Klasse File repräsentiert eine sequentielle Datei. Sie bietet nebenMethoden zur Ein- und Ausgabe elementarer Datentypen (wie zum Beispiel INTEGER)auch die Methoden WriteObject() und ReadObject() an, die zur Ein- und Ausgabevon persistenten Objekten dienen. In diesen wird zunächst <strong>der</strong> dynamische Typ desübergebenen Objekts in geeigneter Form gelesen bzw. geschrieben. Dann wird die entsprechendeMethode aus <strong>der</strong> Klasse Persistent selbst aufgerufen, um die eigentlichenDaten des Objekts zu lesen bzw. zu schreiben. Enthält eine Klasse Verweise auf weitereObjekte, so werden diese in StoreOn() bzw. RestoreFrom() durch erneute (rekursive)Aufrufe von WriteObject() bzw. ReadObject() behandelt. 15 Wir erhalten so einesymmetrische Lösung für das Problem persistenter Objekte, wie sie zum Beispiel auchin [28] diskutiert wurde.Eine konkrete Klasse, die persistent sein soll, leiten wir also von <strong>der</strong> BasisklassePersistent ab und verwenden die entsprechenden Methoden von File, um Instanzenzu lesen bzw. zu schreiben. Problematisch ist dies allerdings, wenn unsere konkreteKlasse im Rahmen einer Anwendung schon Teil einer an<strong>der</strong>en Klassenhierarchie ist,o<strong>der</strong> sie sogar schon einen an<strong>der</strong>en Wesenszug trägt: Wir können sie nicht zusätzlich14 Durch eine universelle Basisklasse würden wir statische Typ-Sicherheit verlieren, was natürlichvermieden werden sollte. In diesem Fall führt Mehrfachvererbung also zu einem Mehr an Sicherheit.15 Für zyklische Strukturen muß natürlich Buch darüber geführt werden, welche Objekte schon verarbeitetwurden.


128 Klassen, Typen und ObjektorientierungListing 6.2 Eine Klasse für sequentielle Dateien und persistente Objekte in (Oberon-2)TYPE Stream = POINTER TO StreamDesc;StreamDesc = RECORD (* ... *) END;PROCEDURE (self: Stream) WriteInt (i: INTEGER);BEGIN (* ... *)END WriteInt;PROCEDURE (self: Stream) ReadInt (VAR i: INTEGER);BEGIN (* ... *)END ReadInt;(* ...other methods for basic types... *)PROCEDURE (self: Stream) WriteObject (p: Persistent);BEGIN(* write dynamic type information *)(* call p.StoreOn (self) *)END WriteObject;PROCEDURE (self: Stream) ReadObject (VAR p: Persistent);BEGIN(* read type information *)(* create object *)(* call p.RestoreFrom (self) *)END ReadObject;auch von Persistent ableiten. Es bliebe nur die Möglichkeit, die Basisklasse <strong>der</strong> gesamtenHierarchie von Persistent abzuleiten, was im Endeffekt zu <strong>der</strong> oben beschriebenenAnsammlung“ von Wesenszügen in einer universellen Basisklasse führt. Eine direkteund flexible Implementierung unseres Entwurfs ist in Oberon-2 offensichtlich nicht”möglich, da Einfachvererbung dazu nicht mächtig genug ist.In [47] gibt Mössenböck ein Entwurfsmuster an, mit dem Mehrfachvererbung aufEinfachvererbung abgebildet werden kann. Betrachten wir den Einsatz dieses Mustersam Beispiel des Problems persistenter Objekte: Hier würden wir die konkrete KlasseConcrete gerne von Persistent sowie von einer an<strong>der</strong>en Klasse Object ableiten. Dazumüßten wir anstatt von Concrete eine Zwillingsklasse“, bestehend aus den separaten”Klassen ConcreteObject und ConcretePersistent (mit ConcreteObject✄Object undConcretePersistent ✄ Persistent), in einem Modul implementieren. Instanzen dieserKlassen würden gemeinsam erzeugt und hätten jeweils eine Referenz auf die Instanz<strong>der</strong> an<strong>der</strong>en Klasse. Soll ein Objekt gespeichert werden, dann übergeben wir das entsprechendeTeilobjekt <strong>der</strong> Klasse ConcretePersistent an File.WriteObject(). DieMethode StoreOn() dieser Klasse würde dann über die Referenz auf sein Partnerobjekt“zugreifen und dessen Datenfel<strong>der</strong> speichern. Offensichtlich ist dieses Entwurfsmu-”ster zwar anwendbar, aber wenig praktikabel, denn es eröffnet eine Reihe von neuenMöglichkeiten, um Fehler zu machen, und verschleiert“ die Klassenhierarchie durch”dynamische Beziehungen, die für Klienten nicht statisch ersichtlich sind.


6.1 Einführung und Motivation 129Allerdings gelten die in Abschnitt 6.1.3 angeführten Gründe zur Trennung von Subtypingund Subclassing auch im Rahmen <strong>der</strong> Mehrfachvererbung: Genaugenommen istMehrfachvererbung in Programmiersprachen wie Eiffel und C++ also sowohl mehrfachesSubtyping als auch mehrfaches Subclassing. Unter mehrfachem Subtyping verstehen wirhier, daß eine Instanz einer Klasse A, die von den Klassen A1, ..., An abgeleitet wurde,überall dort verwendet werden kann, wo auch diese Basisklassen verwendet werdenkönnen. Unter mehrfachem Subclassing verstehen wir hier, daß Elemente <strong>der</strong> Implementierung<strong>der</strong> Klassen A1, ..., An in A wie<strong>der</strong>verwendet werden können.<strong>Zur</strong> Realisierung von Wesenszügen wäre mehrfaches Subtyping aber offensichtlichausreichend: Wir haben sie bisher ja ohnehin stets durch abstrakte Klassen implementiert.Für mehrfaches Subtyping treten aber weniger Probleme auf, als für Mehrfachvererbung(im Sinne von Subtyping und Subclassing). Wir müssen hier nur“ mit <strong>der</strong>”Möglichkeit von Namenskonflikten leben; Probleme wie<strong>der</strong>holter Vererbung treten hingegennicht auf. Außerdem werden Klassenbibliotheken durch mehrfaches Subtypingnicht annähernd so kompliziert wie durch mehrfaches Subclassing: Schließlich tritt dieDAG-Struktur nur zwischen Signaturen und nicht auch zwischen Klassen im Sinne vonImplementierungen auf. 16Vor diesem Hintergrund wird klar, daß es auch in Oberon-2 wünschenswert wäre,mehrfaches Subtyping (nicht aber mehrfaches Subclassing) anzubieten, um die Konstruktionflexiblerer Klassenbibliotheken zu ermöglichen.6.1.5 Kovarianz und KontravarianzWir stellen das Problem von Kovarianz und Kontravarianz im folgenden anhand <strong>der</strong> Signaturenredefinierter Methoden dar. Es existieren aber auch formale Darstellungen, dieüber Vor- und Nachbedingungen redefinierter Methoden sowie Invarianten abgeleiteterKlassen argumentieren, zum Beispiel [19, 20]. Dieses formale Vorgehen haben wir auchin Abschnitt 4.3.1 kurz angedeutet. Das grundlegende Problem wird im folgenden aberauch ohne formale Hilfsmittel deutlich.Objektorientierte Programmiersprachen unterscheiden sich unter an<strong>der</strong>em darin, obsie es redefinierten Methoden gestatten, die Signatur <strong>der</strong> entsprechenden Methoden <strong>der</strong>Basisklasse in gewissen Teilen zu verän<strong>der</strong>n. Eine übliche Anwendung für eine solcheVerän<strong>der</strong>ung sind Methoden, die Vergleiche zwischen zwei Instanzen einer Klasse implementieren.Betrachten wir etwa eine Klassenhierarchie, die zur Modellierung von Lebensformeneingesetzt wird. Sie könnte in etwa wie folgt aufgebaut sein:• Human ✄ Mammal ✄ Lifeform• Cell ✄ Lifeform• Plant ✄ Lifeform16 Auf Fragen <strong>der</strong> Laufzeiteffizienz von Methodenaufrufen für mehrfaches Subtyping gehen wir inAbschnitt 6.3.4 genauer ein.


130 Klassen, Typen und ObjektorientierungWir nehmen an, daß die Basisklasse Lifeform eine Methode EqualTo() exportiert, diees erlaubt, verschiedene Exemplare einer Lebensform miteinan<strong>der</strong> zu vergleichen:EqualTo : Lifeform × Lifeform ↦→ BOOLEANOffensichtlich macht es keinen Sinn, verschiedene Arten von Lebensformen miteinan<strong>der</strong>zu vergleichen: Für zwei Menschen können wir ”Gleichheit“ sinnvoll definieren (etwadurch gleiche äußerliche Merkmale), zwischen einem Menschen und einer Pflanzehingegen nicht. Daraus folgt, daß wir eigentlich nur an Methoden mit den folgendenSignaturen interessiert sind:EqualTo : Human × Human ↦→ BOOLEANEqualTo : Cell × Cell ↦→ BOOLEANEqualTo : Plant × Plant ↦→ BOOLEANEine Implementierung <strong>der</strong> Klasse Lifeform in Oberon-2 würde die Methode EqualTo()wahrscheinlich wie folgt deklarieren:PROCEDURE (self: Lifeform) EqualTo (other: Lifeform): BOOLEANSie wäre natürlich (wie wahrscheinlich die gesamte Klasse) abstrakt, ihre Signatur würdeaber mit <strong>der</strong> oben angegebenen übereinstimmen.Wollen wir nun allerdings die Klasse Plant als eine von Lifeform abgeleitete Klasseimplementieren, so können wir in Oberon-2 nichtPROCEDURE (self: Plant) EqualTo (other: Plant): BOOLEANschreiben, son<strong>der</strong>n müssen die FormPROCEDURE (self: Plant) EqualTo (other: Lifeform): BOOLEANverwenden, da Oberon-2 eine Verän<strong>der</strong>ung <strong>der</strong> Signatur in redefinierten Methoden nichterlaubt. 17 Wir müßten innerhalb <strong>der</strong> Prozedur dann allerdings entsprechende Typ-Prüfungen durchführen, um den Vergleich zu implementieren:PROCEDURE (self: Plant) EqualTo (other: Lifeform): BOOLEANVAR result: BOOLEAN;BEGINWITH other: Plant DO(* ...result durch Vergleiche einzelner Fel<strong>der</strong> setzen... *)ELSE(* falscher Typ für other, Gleichheit nicht möglich *)result := FALSE;END;RETURN resultEND EqualTo;17 Wenn wir auch den Empfängerparameter als Teil <strong>der</strong> Signatur ansehen, dann erlaubt Oberon-2 nurdie Verän<strong>der</strong>ung dieses einen Parameters.


6.1 Einführung und Motivation 131Die in Oberon-2 verwendete Regel, daß <strong>der</strong> Typ eines Parameters einer Methode in abgeleitetenKlassen nicht durch einen von diesem Typ abgeleiteten Typ ersetzt werdendarf, bezeichnen wir als kontravariant. Die an<strong>der</strong>e Regel, also daß <strong>der</strong> Typ eines Parameterseiner Methode in abgeleiteten Klassen durch einen von diesem Typ abgeleitetenTyp ersetzt werden darf, bezeichnen wir als kovariant. 18Kovarianz wird zum Beispiel in <strong>der</strong> Programmiersprache Eiffel verwendet (siehe [45,S. 142ff]). Der Vorteil von Kovarianz liegt auf <strong>der</strong> Hand: Auch ohne Kenntnisse überdie Implementierung <strong>der</strong> Methode EqualTo() erkennt ein Klient anhand <strong>der</strong> Signatur<strong>der</strong> Methode, welche Art von Lebensform erwartet wird. Dies wäre insbeson<strong>der</strong>e dannwichtig, wenn wir statt <strong>der</strong> WITH-Anweisung einzelne Typ-Zusicherungen o<strong>der</strong> die StandardprozedurASSERT() verwendet hätten, um den richtigen Typ von other sicherzustellen.Dann könnte es nämlich ohne ”Verschulden“ des Klienten zu einem Laufzeitfehlerkommen, <strong>der</strong> aus <strong>der</strong> Signatur <strong>der</strong> Methode nicht ersichtlich ist.Die Frage, die wir uns nun stellen müssen, lautet, ob Kovarianz nicht eine bessereAlternative für Oberon-2 wäre. Dies ist aber zu verneinen, denn Aufrufe kovarianterMethoden sind nicht statisch prüfbar! Warum dies so ist, erkennen wir an folgendemBeispiel, in dem wir Oberon-2 Kovarianz unterstellen:(* ... *)VARl1,l2: Lifeform;h1: Human;b: BOOLEAN;(* ... *)BEGINb := l1.EqualTo (l1); (* works *) (nicht für abstrakte Methode)b := l1.EqualTo (h1); (* works *)b := h1.EqualTo (h1); (* works *)l1 := h1; (* polymorphism *)b := l1.EqualTo (l2); (* crash! *) (runtime)(* ... *)Da die Zuweisung l1 := h1 dendynamischenTypvonl1 auf Human setzt und auch dieMethode EqualTo() damit ihre Signatur än<strong>der</strong>t, müßte <strong>der</strong> Compiler hier automatischeine entsprechende Typ-Prüfung <strong>der</strong> Form l2 IS Human generieren. Das ist genau <strong>der</strong>Test, den wir bei Kontravarianz selbst durchführen mußten. Wir schließen daraus, daßKovarianz von Methodensignaturen nicht zu einer sonst sicheren Programmiersprachewie Oberon-2 paßt.Wenn wir in diesem Zusammenhang an die in Kapitel 4 vorgestellten Konzepte denken,könnten wir uns eine mögliche Lösung des Konflikts zwischen Kovarianz und Kon-18 Die Verwendung von ”Ko-“ und ”Kontra-“ in diesem Zusammenhang läßt sich damit erklären, daß<strong>der</strong> Typ des Empfängerparameters sich ja stets än<strong>der</strong>n darf (sogar muß!). Kovarianz erlaubt es, dieTypen <strong>der</strong> Parameter einer Methode ”in <strong>der</strong> gleichen Weise“ zu verän<strong>der</strong>n, Kontravarianz dagegenerlaubt eine solche Verän<strong>der</strong>ung nicht.


132 Klassen, Typen und Objektorientierungtravarianz mit Hilfe von Vorbedingungen vorstellen. Die Klasse Human müßte dazu dieMethode EqualTo() wie folgt deklarieren:PROCEDURE (self: Human) EqualTo (other: Lifeform): BOOLEAN;REQUIRES other IS Human; (* ... *)Dadurch wäre die Signatur <strong>der</strong> Methode immer noch kontravariant, Klienten würdenaber durch die Vorbedingung explizit darauf hingewiesen, welchen Typ <strong>der</strong> Parameterother hier haben muß. Das Problem ist allerdings, daß eine Vorbedingung dieser Formstärker wäre als die <strong>der</strong> Basisklasse Lifeform. Vorbedingungen dürfenaberinabgeleitetenKlassen nur abgeschwächt werden, eine Lösung des Kovarianzproblems ist alsoauch bei <strong>der</strong> Verwendung von Vorbedingungen nicht möglich.6.2 Spracherweiterungen6.2.1 Ein neuer Sichtbarkeitsbereich für KlassenDie Sichtbarkeit von Bezeichnern über Modulgrenzen hinweg wird in Oberon-2 generellüber Exportmarken festgelegt. Im allgemeinen wird die schon in Oberon vorhandeneMarke *“ benutzt, um einen Bezeichner zu exportieren. Für Variablen und Fel<strong>der</strong> von”Recordtypen bedeutet diese Art des Exports insbeson<strong>der</strong>e, daß sie in externen Modulenals Ziel einer Zuweisung verwendet werden dürfen und somit verän<strong>der</strong>t werden können.Mit Oberon-2 wurde weiterhin die Marke -“eingeführt, <strong>der</strong>en Verwendung nur für”Variablen und Fel<strong>der</strong> von Recordtypen definiert ist. Sie kennzeichnet den jeweiligenBezeichner als schreibgeschützt, er kann also in externen Modulen nicht als Ziel einerZuweisung o<strong>der</strong> einer an<strong>der</strong>en Operation, die den Wert <strong>der</strong> an ihn gebundenen Variableverän<strong>der</strong>n könnte, verwendet werden. An<strong>der</strong>e Bezeichner, etwa von Prozeduren o<strong>der</strong>Konstanten, können zwar auch durch die Marke -“ exportiert werden, semantisch bestehtfür sie aber kein Unterschied zum Export durch *“. 19 ””Es liegt natürlich nahe, den in Abschnitt 6.1.1 motivierten neuen Sichtbarkeitsbereichfür Klassen ebenfalls durch eine solche Marke zu kennzeichnen. Um nicht zu sehr ausdem durch Oberon-2 vorgegebenen Rahmen zu fallen benutzten wir hier die Marke +“. ”Ein neues Schlüsselwort wie PROTECTED o<strong>der</strong> eine an<strong>der</strong>e Marke wie &“wäre natürlich”ebenfalls möglich, die Verwendung von +“fügt sich aber besser in das Gesamtbild von”<strong>Fro<strong>der</strong>on</strong>-1 ein.Die Frage, für welche Bezeichner die neue Exportmarke zulässig ist, läßt sich leichtbeantworten: Klassen werden in Oberon-2 durch (erweiterbare) Recordtypen und typgebundeneProzeduren realisiert, folglich lassen wir die Verwendung von +“ auch nur”für die Bezeichner von Fel<strong>der</strong>n eines Recordtyps o<strong>der</strong> typgebundene Prozeduren zu. 2019 Die hier fehlende Definition <strong>der</strong> Semantik bzw. <strong>der</strong> Zulässigkeit von ”-“können wir als weitereSchwachstelle <strong>der</strong> Sprachdefinition [46] werten.20 Diese aus <strong>der</strong> Motivation in Abschnitt 6.1.1 nicht direkt hervorgehende Erweiterung auf typgebundeneProzeduren ist zum Beispiel bei <strong>der</strong> Implementierung <strong>der</strong> in [21] vorgestellten EntwurfsmusterTemplate Method o<strong>der</strong> Proxy sinnvoll.


6.2 Spracherweiterungen 133Im Gegensatz zu -“führt eine Verwendung für an<strong>der</strong>e Bezeichner zu einer Fehlermeldungdes Compilers. Da Klassen (und Methoden) in Oberon-2 stets im globalen”Sichtbarkeitsbereich eines Moduls deklariert werden müssen, sollten wir natürlich aucheine Verwendung <strong>der</strong> Exportmarke in lokal deklarierten Recordtypen verbieten. DiesesVerbot ist in Oberon-2 aber schon vorhanden, da lokale Bezeichner generell nichtexportiert werden dürfen.Listing 6.3 Beispiel für die Verwendung des geschützten ExportsMODULE Protected;TYPEClass* = POINTER TO ClassDesc;ClassDesc* = RECORDa, b-, c*, d+: INTEGER;END;PROCEDURE (self: Class) Do* (p: INTEGER);END Do;PROCEDURE (self: Class) DoProtected+ (p: INTEGER);END DoProtected;PROCEDURE DoProcedure* (class: Class; p: INTEGER);END DoProcedure;END Protected.Betrachten wir als Beispiel für die Verwendung unseres neuen Sichtbarkeitsbereichsdas in Listing 6.3 gezeigte Modul. Die in <strong>der</strong> Klasse Class deklarierten Bezeichner a, b, c,bzw. d sind jeweils privat, schreibgeschützt exportiert, öffentlich exportiert bzw. geschütztexportiert. DieMethodeDo() sowie die Prozedur DoProcedure() sind öffentlich exportiert,dieMethodeDoProtected() hingegen ist geschützt exportiert.Listing 6.4 Schnittstelle für ”normale“ KlientenDEFINITION Protected;TYPEClass = POINTER TO ClassDesc;ClassDesc = RECORDb-, c: INTEGER;END;PROCEDURE (self: Class) Do (p: INTEGER);PROCEDURE DoProcedure (class: Class; p: INTEGER);END Protected.Bedingt durch die Semantik des neuen Sichtbarkeitsbereichs hat dieses Modul natürlichzwei verschiedene Schnittstellen: Eine für ”normale“ Klienten wie in Listing 6.4gezeigt, und eine für Methoden, die innerhalb eines Klienten an eine von Class abgeleiteteKlasse gebunden sind, wie in Listing 6.5 gezeigt. Ein ”normaler Klient“ könntealso sowohl Do() als auch DoProcedure() aufrufen, den Wert von c beliebig verwenden


134 Klassen, Typen und ObjektorientierungListing 6.5 Schnittstelle in Methoden einer abgeleiteten KlasseDEFINITION Protected;TYPEClass = POINTER TO ClassDesc;ClassDesc = RECORDb-, c, d: INTEGER;END;PROCEDURE (self: Class) Do (p: INTEGER);PROCEDURE (self: Class) DoProtected (p: INTEGER);PROCEDURE DoProcedure (class: Class; p: INTEGER);END Protected.und den Wert von b lesen. Innerhalb einer entsprechenden Methode könnten dagegenbeliebige Zugriffe auf d erfolgen, und die Methode DoProtected() könnte aufgerufenwerden; b wäre aber weiterhin schreibgeschützt.An dieser Stelle sind noch eine Reihe von <strong>weiteren</strong> Fragen bezüglich <strong>der</strong> Verwendungvon geschützt exportierten Bezeichnern offen:1. Warum ist die in Listing 6.5 gezeigte Schnittstelle nur innerhalb von Methodenabgeleiteter Klassen gültig?2. Können Methoden, die geschützt exportiert wurden, in abgeleiteten Klassen redefiniertwerden?3. Ist <strong>der</strong> Zugriff auf geschützte Bezeichner innerhalb von Methoden nur für denEmpfängerparameter erlaubt o<strong>der</strong> auch für an<strong>der</strong>e Variablen des Typs, <strong>der</strong> Bezeichnergeschützt exportiert?4. Wie behandeln wir lokal zu einer Methode deklarierte Prozeduren? Ist dort <strong>der</strong>Zugriff auf geschützte Bezeichner ebenfalls erlaubt, wenn er in <strong>der</strong> Methode selbsterlaubt ist?Die erste Frage läßt sich relativ leicht beantworten: Wir wollen in einer abgeleitetenKlasse ja möglichst viel Funktionalität <strong>der</strong> Basisklasse wie<strong>der</strong>verwenden und das auchmöglichst effizient, also ohne ”Umwege“ über Zugriffsmethoden. Methoden von abgeleitetenKlassen sind deshalb <strong>der</strong> einzige Ort, an dem wir die geschützt exportiertenBezeichner sinnvoll nutzen können.Die zweite Frage ist ebenfalls leicht zu beantworten: Methoden, die geschützt exportiertwurden, dürfen in abgeleiteten Klassen redefiniert werden. Einzige Bedingung ist,daß diese Methoden ebenfalls wie<strong>der</strong> geschützt exportiert sind. Diese Bedingung gilt inOberon-2 aber auch für an<strong>der</strong>e Methoden, weswegen wir sie nicht ausdrücklich betonenmüssen. Anwendungen für die Redefinition solcher Methoden gibt es viele, zum Beispielbei <strong>der</strong> Implementierung einiger <strong>der</strong> in [21] vorgestellten Entwurfsmuster.Die dritte Frage müssen wir aber eingehen<strong>der</strong> diskutieren: Sie betrifft Methoden vonabgeleiteten Klassen, in denen statisch nicht nur <strong>der</strong> Empfängerparameter, son<strong>der</strong>n auch


6.2 Spracherweiterungen 135weitere Variablen vom Typ des Empfängerparameters sichtbar sind. Zunächst würde esvom Standpunkt <strong>der</strong> Lokalität aus betrachtet naheliegen, Zugriffe auf geschützt exportierteBezeichner nur dann zu erlauben, wenn sie den Empfängerparameter betreffen.Dies würde aber für einige übliche Operationen, die in Klassenhierarchien verwendetwerden, zu Problemen führen. Zwei Beispiele für solche Operationen sind Vergleiche(o<strong>der</strong> auch Ordnungen, die auf den Objekten definiert sind) sowie die Erzeugung einerKopie eines Objekts. Hier sind unter Umständen sowohl Zugriffe auf Fel<strong>der</strong> desEmpfängerparameters als auch Zugriffe auf Fel<strong>der</strong> eines <strong>weiteren</strong> Parameters des gleichenTyps notwendig. Deswegen sollten wir den Zugriff für entsprechende Parametereiner Methode auf jeden Fall erlauben. Wollen wir aber den Zugriff nur auf Parameterbeschränken, würden wir eine Unregelmäßigkeit in die Sprache einführen, die wir nichtleicht erklären könnten. Deswegen entschließen wir uns dazu, entsprechende Zugriffe aufalle statisch sichtbaren Variablen des entsprechenden Typs zu erlauben.Eine Antwort auf die vierte und letzte Frage ist dagegen wie<strong>der</strong> einfach zu geben: Methodensollten eine gewisse Komplexität bzw. einen gewissen (textuellen) Umfang nichtübersteigen. Deswegen kann es sinnvoll sein, lokal zu einer Methode gewisse Prozedurenzu deklarieren, die Teile <strong>der</strong> zu realisierenden Operation kapseln. Da diese Prozedurenjedoch Teil“ <strong>der</strong> Methode sind, sollten sie ebenfalls Zugriff auf die entsprechenden”Bezeichner haben.6.2.2 Abstrakte Klassen und MethodenDie Verwendung von abstrakten Klassen und Methoden sowie ihre explizite Kennzeichnungals solche wurde in Abschnitt 6.1.2 motiviert. Für die <strong>Entwicklung</strong> einer Spracherweiterungkönnen wir zunächst das Folgende festhalten:Eine Klasse, in <strong>der</strong> eine abstrakte Methode deklariert ist, ist ebenfalls abstrakt.Allerdings können innerhalb einer abstrakten Klasse durchaus auchkonkrete Methoden implementiert sein.Mit an<strong>der</strong>en Worten würde es also ausreichen, wenn wir lediglich abstrakte Methodenkennzeichnen, da sie eine abstrakte Klasse implizieren.Es gibt natürlich eine Reihe von verschiedenen Möglichkeiten, diese Kennzeichungdurchzuführen. Eine erste Möglichkeit wäre es, Methoden, <strong>der</strong>en Rumpf leer ist, alsabstrakte Methoden zu betrachten. Die Syntax von Prozedurdeklarationen würde dieszulassen, da <strong>der</strong> BEGIN-Teil optional ist:ProcDecl = PROCEDURE [Receiver] IdentDef [FormalPars] ";"DeclSeq [BEGIN StatementSeq] END ident.Hier wären also keine syntaktischen Än<strong>der</strong>ungen nötig, lediglich die Semantik einesfehlenden BEGIN“ würde sich im Vergleich zu Oberon-2 än<strong>der</strong>n. So wäre die Methode”PROCEDURE (self: Class) Do ();END Do;


136 Klassen, Typen und Objektorientierungabstrakt, während die MethodePROCEDURE (self: Class) Do ();BEGINEND Do;zwar konkret aber ohne Funktionalität wäre. Gerade diese Än<strong>der</strong>ung <strong>der</strong> Semantik istaber die Schwachstelle dieses Vorschlags, denn ein legales Oberon-2-Programm wäredann unter Umständen kein legales <strong>Fro<strong>der</strong>on</strong>-1-Programm mehr. Zum Beispiel könnenin Oberon-2 von einer Klasse, in <strong>der</strong> Methoden ohne Rumpf deklariert sind, Instanzenerzeugt werden, während in <strong>Fro<strong>der</strong>on</strong>-1 für den gleichen Quelltext eine Fehlermeldungdarauf hinweisen würde, daß diese Klasse abstrakt ist. Dieser Bruch <strong>der</strong> Kompatibilitätwürde aber den Zielen dieser Arbeit wi<strong>der</strong>sprechen, weswegen wir den Vorschlag wie<strong>der</strong>verwerfen.In [61] schlägt Josef Templ zwei weitere Möglichkeiten vor, die ebenfalls dadurchgekennzeichnet sind, daß sie keine neuen Elemente in <strong>der</strong> Sprache verlangen:• Abstrakte Methoden werden innerhalb <strong>der</strong> Deklaration des Recordtyps, an den siegebunden sind, deklariert. Sie werden dabei ähnlich wie Vorwärtsdeklarationenbehandelt, das heißt, sie dürfen insbeson<strong>der</strong>e keinen Rumpf haben.• Vorwärtsdeklarationen von Methoden, die innerhalb eines Moduls nicht aufgelöst(das heißt an konkrete Methoden gebunden) werden, werden als abstrakte Methodenbetrachtet.Die erste Möglichkeit würde eine Verän<strong>der</strong>ung <strong>der</strong> Syntax für die Deklaration von Recordtypenverlangen. An die Stelle <strong>der</strong> in Oberon-2 verwendeten SyntaxType = ...| RECORD ["(" Qualident ")"] FieldList {";" FieldList} END| ....FieldList = [IdentList ":" Type].würde dann beispielsweise die folgende Syntax treten:Type = ...| RECORD ["("Qualident")"] AbstractDecl {";" AbstractDecl}FieldList {";" FieldList} END| ....AbstractDecl = [PROCEDURE Receiver IdentDef [FormalPars]].Die zweite Möglichkeit dagegen würde (wie unser erster Vorschlag) auf jegliche syntaktischeÄn<strong>der</strong>ung verzichten und lediglich einem bisherigen Konstrukt eine an<strong>der</strong>e Semantikgeben. Im Unterschied zu unserem Vorschlag hätte die Än<strong>der</strong>ung <strong>der</strong> Semantik hier aber


6.2 Spracherweiterungen 137keine negativen Konsequenzen für die Kompatibilität zu Oberon-2. Zwei Probleme tretenaber auch hier auf: Zum einen erscheint es fraglich, ob Vorwärtsdeklarationen fürabstrakte Methoden mißbraucht“ werden sollten, da sie in Oberon-2 einem gänzlich an<strong>der</strong>enZweck dienen. Zum an<strong>der</strong>en ist es nicht son<strong>der</strong>lich elegant, eine Spracherweiterung”einzuführen, die einem semantisch fehlerhaften Konstrukt von Oberon-2 nachträglicheinen Sinn gibt. Beide Möglichkeiten wären aber grundsätzlich kompatibel mit bestehendenOberon-2-Programmen, könnten also auch im Rahmen dieser Arbeit verwendetwerden.Den drei bis jetzt diskutierten Vorschlägen ist gemeinsam, daß sie die jeweilige Klassenicht explizit als abstrakt kennzeichnen. Dies entspricht unserer eingangs gemachtenFeststellung, daß abstrakte Methoden eine abstrakte Klasse implizieren. Die fehlendeexplizite Kennzeichnung <strong>der</strong> Klasse kann aber zu Problemen führen:• Um eine Klasse abstrakt zu machen, muß stets mindestens eine abstrakte Methodevorhanden sein, was aber im Rahmen bestimmter Anwendungen nicht unbedingtsinnvoll sein muß (es könnte durchaus eine abstrakte Klasse mit keiner einzigenabstrakten Methode geben).• Ob eine Klasse abstrakt o<strong>der</strong> konkret ist, läßt sich nicht mit Sicherheit anhand <strong>der</strong>Schnittstelle entscheiden. 21• Eine bestehende Klasse könnte ”aus Versehen“ durch die Än<strong>der</strong>ung einer Methodeabstrakt gemacht werden, was eine erneute Übersetzung aller Klienten erfor<strong>der</strong>t,da diese eventuell Instanzen <strong>der</strong> Klasse anlegen wollen.Diese Probleme lassen sich vermeiden, indem wir sowohl Klassen als auch Methodenexplizit als abstrakt kennzeichnen.Um Klassen in dieser Weise zu kennzeichnen, müssen wir die Syntax <strong>der</strong> Deklarationvon Recordtypen verän<strong>der</strong>n. Prinzipiell haben wir entwe<strong>der</strong> die Möglichkeit, diebestehende Syntax so zu verän<strong>der</strong>n, daß sie auch abstrakte Klassen umfaßt, o<strong>der</strong> eineneue Produktion einzuführen, die nur für abstrakte Klassen verwendet wird. Um unsereErweiterung leichter integrieren zu können (und um an <strong>der</strong> bestehenden Syntax so wenigwie möglichzuverän<strong>der</strong>n), entscheiden wir uns für die zweite Variante und ergänzen dieProduktion Type entsprechend:Type = ...| RECORD ["(" Qualident ")"] FieldList {";" FieldList} END| ABSTRACT ["(" Qualident ")"] FieldList {";" FieldList} END| ....Die Deklaration eines Recordtyps für eine abstrakte Klasse erfolgt also nach obigerSyntax durch das neue Schlüsselwort ABSTRACT. Konkrete Klassen werden nach wie vordurch RECORD deklariert.21 Natürlich ist dieses Argument nur gültig, solange es kein Werkzeug zur Erzeugung von Schnittstellengibt, das abstrakte Klassen berücksichtigt und entsprechend kennzeichnet.


138 Klassen, Typen und ObjektorientierungJetzt müssen wir noch die Deklaration <strong>der</strong> abstrakten Methoden definieren.wollen dazu die folgenden beiden Vorschläge in die engere Wahl ziehen:• Abstrakte Methoden werden (wie auch von Templ vorgeschlagen) innerhalb <strong>der</strong>abstrakten Klasse deklariert.• Abstrakte Methoden werden wie an<strong>der</strong>e Prozeduren und Methoden deklariert, verwendenaber anstatt des üblichen BEGIN-Teils das neue Schlüsselwort ABSTRACT.Betrachten wir beide Vorschläge zunächst unter dem Gesichtspunkt, wie gut sie sichin Oberon-2 einfügen. Der zweite Vorschlag bleibt offensichtlich näher an Oberon-2,da lediglich eine neue Alternative zur Deklaration einer Prozedur eingeführt wird. Dererste Vorschlag verlagert dagegen die Deklaration von abstrakten Methoden an einenOrt, <strong>der</strong> in Oberon-2 normalerweise nicht für Prozedurdeklarationen verwendet werdenkann. Unter diesem Aspekt wäre also <strong>der</strong> zweite Vorschlag zu bevorzugen.Wenn wir die beiden Vorschläge jedoch unter einem an<strong>der</strong>en Gesichtspunkt betrachten,dann än<strong>der</strong>t sich diese Situation schlagartig: Welche zusätzlichen Regeln sind in<strong>der</strong> Sprachdefinition nötig, um die korrekte Verwendung <strong>der</strong> neuen Konstrukte sicherzustellen?Für den zweiten Vorschlag müßten wir offensichtlich Regeln <strong>der</strong> folgenden Arteinführen:Das Schlüsselwort ABSTRACT darf nur in Methoden verwendet werden, die aneine abstrakte Klasse gebunden sind.Für den ersten Vorschlag sind solche Regeln nicht notwendig, da schon syntaktisch sichergestelltist, wo und wie abstrakte Methoden deklariert werden dürfen. Unter diesemGesichtspunkt wäre also <strong>der</strong> erste Vorschlag vorzuziehen.In <strong>Fro<strong>der</strong>on</strong>-1 entscheiden wir uns deswegen für den ersten Vorschlag. Außerdemkönnen wir den scheinbaren Nachteil dieser Lösung weiter entkräften: Während Templabstrakte Methoden in die Deklaration gewöhnlicher Recordtypen einbringt, tun wirdies nur in <strong>der</strong> — ohnehin von uns eingeführten — Produktion für abstrakte Klassen.Wir verän<strong>der</strong>n also kein Konstrukt, das in Oberon-2 selbst vorhanden ist, son<strong>der</strong>n wirerweitern ein <strong>Fro<strong>der</strong>on</strong>-1-Konstrukt um zusätzliche Möglichkeiten. Deswegen könnenwir uns ”guten Gewissens“ für die folgende, endgültige Syntax entscheiden:Type = ...| ABSTRACT ["(" Qualident ")"] FieldList {";" FieldList}{AbstractDecl ";"} END| ....AbstractDecl = PROCEDURE Receiver IdentDef [FormalPars].Diese Syntax hat natürlich außerdem den Vorteil, daß es rein syntaktisch unmöglichist, eine gewöhnliche (nicht typgebundene) Prozedur als ”abstrakt“ zu deklarieren (dieswäre eine weitere Regel gewesen, die <strong>der</strong> zweite Vorschlag notwendig gemacht hätte). 2222 Im Vergleich zu <strong>der</strong> weiter oben für den Vorschlag von Templ angegebenen Syntax ist die hierverwendete Form außerdem frei von LL(1)-Konflikten.Wir


6.2 Spracherweiterungen 139Einige Regeln sind aber auch bei dieser Form <strong>der</strong> Syntax noch notwendig, und ihreEinhaltung soll natürlich auch durch den Compiler geprüft werden:• Eine abstrakte Klasse darf lediglich von einer an<strong>der</strong>en abstrakten Klasse abgeleitetwerden, nicht aber von einer konkreten Klasse.• Konkrete Klassen, die von einer abstrakten Klasse abgeleitet sind, müssen alleabstrakten Methoden implementieren (das heißt durch konkrete Methoden redefinieren).• Für die innerhalb einer abstrakten Klasse deklarierten abstrakten Methoden dürfennicht gleichzeitig auch konkrete Methoden deklariert werden (eine Implementierungdarf erst in abgeleiteten Klassen erfolgen).• Von abstrakten Klassen dürfen keine Instanzen angelegt werden.• Ein Aufruf <strong>der</strong> Form self.X^() innerhalb einer konkreten Methode X ist verboten,wenn die überschriebene Methode abstrakt ist.Die erste Regel benötigen wir, um zu verhin<strong>der</strong>n, daß Klassen nachträglich“ abstrakt”gemacht werden können. Die zweite Regel zwingt den Programmierer zur vollständigenImplementierung aller abstrakten Methoden <strong>der</strong> abstrakten Klasse. Sinn <strong>der</strong> drittenRegel ist es, den Programmierer nicht dazu zu verleiten, alle Methoden einer abstraktenKlasse vorsorglich“ als abstrakt zu deklarieren und dann einige davon nachträglich“” ”doch konkret zu machen.Die vierte Regel drückt das aus, was uns ursprünglich zur Integration abstrakterKlassen in Oberon-2 motiviert hat. Sei ADesc eine abstrakte Klasse und APtr ein ansie gebundener Zeigertyp. Dann sind sowohl die Deklaration VAR a: ADesc als auch <strong>der</strong>Aufruf NEW(p) —wennVAR p: APtr deklariert wurde — verboten. Zuweisungen einesZeigers vom Typ CPtr — <strong>der</strong> an eine konkrete Klasse CDesc ✄ ADesc gebunden ist — anden Zeiger p sind allerdings erlaubt, ebenso wie die Verwendung von ADesc als Typ einesReferenzparameters. Für die dynamischen Typen solcher Variablen gelten die üblichenRegeln von Oberon-2, insbeson<strong>der</strong>e sind Typ-Zusicherungen und Typ-Prüfungen auchfür abstrakte Klassen erlaubt. 23 Die letzte Regel bewahrt uns schließlich davor, dochaus Versehen“ eine abstrakte Methode aufrufen zu können. Alle an<strong>der</strong>en Möglichkeiten”eines solchen Aufrufs wurden schon durch die übrigen Regeln ausgeschlossen.Damit sind unsere Spracherweiterungen für abstrakte Klassen und Methoden abgeschlossen.Lediglich eine Anmerkung wollen wir noch machen: Abstrakte Methodensollten stets in passen<strong>der</strong> Weise (also durch *“bzw. +“) exportiert sein, damit sie in” ”externen Modulen auch redefiniert werden können.23 Da also praktisch nur Referenzen auf abstrakte Klassen relevant sind, könnte man geneigt sein,durch das Schlüsselwort ABSTRACT einen Zeigertyp zu deklarieren. Allerdings würde dies nicht gut zuOberon-2 passen: Zum einen werden Zeiger in Oberon-2 immer explizit deklariert, zum an<strong>der</strong>en werdenauch dynamisch typisierte VAR-Parameter unterstützt, die häufig eingesetzt werden.


140 Klassen, Typen und Objektorientierung6.2.3 SignaturenDie Trennung von Spezifikation und Implementierung für Klassen sowie die damit verbundeneTrennung von Subtyping und Subclassing wurde in Abschnitt 6.1.3 motiviert.In diesem Abschnitt wollen wir geeignete Spracherweiterungen entwickeln, um dieseTrennung in <strong>Fro<strong>der</strong>on</strong>-1 explizit durchführen zu können.Zunächst müssen wir bezüglich <strong>der</strong> Kompatibilität zu Oberon-2 folgendes feststellen:Wir können nicht darauf verzichten, daß auch die normale Typerweiterung“ weiterhin”eine Subtyping-Relation einführt. Warum dies so ist, liegt auf <strong>der</strong> Hand: Eine Reduktion<strong>der</strong> normalen Typerweiterung“ auf reines Subclassing würde dazu führen, daß”legale Oberon-2-Programme keine legalen <strong>Fro<strong>der</strong>on</strong>-1-Programme mehr wären, was wirvermeiden wollen.Offensichtlich aber muß — wie schon in Abschnitt 6.2.2 — zur Trennung von Spezifikationund Implementierung die Schnittstelle einer Klasse deklariert werden, ohnejedoch gleichzeitig eine Implementierung anzugeben. Diese Verwandtschaft zu den abstraktenKlassen legt eine Wie<strong>der</strong>verwendung <strong>der</strong> dort erarbeiteten Syntax nahe, etwain <strong>der</strong> folgenden Form:Type = ...| ABSTRACT ["(" Qualident ")"] FieldList {";" FieldList}{AbstractDecl ";"} END| SIGNATURE ["(" Qualident ")"] {AbstractDecl ";"} END| ....AbstractDecl = PROCEDURE Receiver IdentDef [FormalPars].Die Wahl des Schlüsselworts SIGNATURE für die Schnittstelle einer Klasse ist relativwillkürlich, ebenso hätte zum Beispiel INTERFACE o<strong>der</strong> DEFINITION verwendet werdenkönnen. Während abstrakte Klassen die Implementierung jedoch nur teilweise offenlassen,sollten Signaturen sie vollständig an abgeleitete Klassen“ — o<strong>der</strong> vielmehr solche”Klassen, die diese Schnittstelle implementieren wollen — delegieren. Für Signaturenmacht deswegen die Deklaration von Datenfel<strong>der</strong>n ebensowenig Sinn wie die Deklarationkonkreter Methoden. Ersteres äußert sich in obiger Syntax, letzteres muß durch einesemantische Prüfung im Compiler sichergestellt werden.Natürlich müssen wir für konkrete Klassen die Möglichkeit schaffen, auszudrücken,daß sie eine bestimmte Signatur implementieren. Wir könnten dazu die schon bestehendeSyntax zur Angabe des Basistyps verwenden und auch Signaturen in Klammernhinter dem Schlüsselwort RECORD erlauben. Allerdings ist diese Notation ja schon fürdie gewöhnliche“ Typerweiterung (bzw. Vererbung) reserviert, und sie zu überladen”scheint — insbeson<strong>der</strong>e wegen <strong>der</strong> unterschiedlichen Semantik von Subtyping und Subclassing— keine gute Idee zu sein. Wir verwenden deshalb ein an<strong>der</strong>es Klammerpaarund kommen damit zu folgen<strong>der</strong> Syntax für die Deklaration von (abstrakten und konkreten)Klassen:Type = ...


6.2 Spracherweiterungen 141| RECORD ["(" Qualident ")"] ["{" Qualident "}"]FieldList {";" FieldList} END| ABSTRACT ["(" Qualident ")"] ["{" Qualident "}"]FieldList {";" FieldList}{AbstractDecl ";"} END| ....Beide Arten von Klassen können nun durch Angabe eines geeigneten Bezeichners ingeschweiften Klammern kennzeichnen, daß sie die entsprechende Signatur implementieren.Das heißt, daß sie eine (abstrakte o<strong>der</strong> konkrete) Methode für jede Methode <strong>der</strong>Signatur deklarieren. Damit haben wir die notwendige Verbindung zwischen den beidenHierarchien (von Signaturen und Klassen) geschaffen. Diese Verbindung ist außerdemunabhängig von <strong>der</strong> ”gewöhlichen“ Vererbung, so daß die in Abschnitt 6.1.3 angesprochenenProbleme für ”simulierte Signaturen“ nicht auftreten.Da wir für Klassen jetzt eine explizite Möglichkeit geschaffen haben, durch Angabeeines Bezeichners in geschweiften Klammern zu kennzeichnen, daß sie die entsprechendeSignatur implementieren, sollten wir uns nochmals <strong>der</strong> Syntax von Signaturen selbstzuwenden. Dort verwenden wir nämlich (siehe oben) bis jetzt runde Klammern, umdieSubtyping-Relation zwischen einzelnen Signaturen auszudrücken. Um für den Programmierereine bessere Assoziation im Sinne von( Bezeichner ) → Subclassing { Bezeichner }→Subtypingzu schaffen 24 , wollen wir auch dort geschweifte Klammern einsetzen:Type = ...| SIGNATURE ["{" Qualident "}"] {AbstractDecl ";"} END| ....Damit haben wir die syntaktischen Erweiterungen für Signaturen abgeschlossen. Dievom Compiler durchzuführenden semantischen Prüfungen, die auch Regeln im Sinne<strong>der</strong> Sprachdefinition darstellen, lauten ähnlich wie schon für abstrakte Klassen:• Signaturen dürfen lediglich von an<strong>der</strong>en Signaturen ”abgeleitet“ werden, nicht abervon (abstrakten o<strong>der</strong> konkreten) Klassen.• Klassen (konkrete o<strong>der</strong> abstrakte), die eine Signatur implementieren wollen, müssenalle Methoden <strong>der</strong> Signatur redefinieren.• An Signaturen können keine konkreten Methoden gebunden werden.• Von Signaturen dürfen keine Instanzen angelegt werden.24 Natürlich wäre auch eine an<strong>der</strong>e Assoziation <strong>der</strong> Form ”( Bezeichner ) → Subclassing/Subtyping“bzw. ”{ Bezeichner }→Implementierung“ möglich. Sie wird zum Beispiel in JAVA verwendet unddurch die Schlüsselwörter extends bzw. implements ausgedrückt.


142 Klassen, Typen und Objektorientierung• Ein Aufruf <strong>der</strong> Form self.X^() innerhalb einer konkreten Methode X ist verboten,wenn die überschriebene Methode in einer Signatur deklariert wurde.Auffallend ist hier im Vergleich zu Abschnitt 6.2.2, daß eine abstrakte Klasse eine Signaturimplementieren“ kann, ohne alle Methoden <strong>der</strong> Signatur durch konkrete Methoden”zu redefinieren. Dies ist aber nötig, damit abstrakten Klassen nichts von ihrem primärenZweck, mehr Freiheit für von Ihnen abgeleitete konkrete Klassen zu erlauben, genommenwird. Außerdem sind abstrakte Klassen ja Teil <strong>der</strong> Subclassing-Hierarchie, so daß es imInteresse <strong>der</strong> Kopplung bei<strong>der</strong> Hierarchien sinnvoll ist, sie nicht von <strong>der</strong> Verwendungvon Signaturen auszuschließen.Für Signaturen müssen wir uns schließlich noch <strong>der</strong> Frage zuwenden, wie wir dieKompatibilität zwischen Referenzen auf (abstrakte o<strong>der</strong> konkrete) Klassen und Referenzenauf Signaturen definieren wollen. 25 Allgemein sind zwei Referenzen nur dannkompatibel zueinan<strong>der</strong>, wenn sie in einer (direkten o<strong>der</strong> indirekten) Subtyping-Relationstehen. Betrachten wir für Signaturen dazu folgendes Beispiel:TYPE S = POINTER TO SD;SD = SIGNATURE END;C = POINTER TO CD;CD = RECORD {SD} END;VAR s: S; c: C;Hier wäre eine Zuweisung s := c auf jeden Fall erlaubt, da C ✄ S gilt. Die umgekehrteZuweisung müßte hingegen (wie auch in Oberon-2) durch eine Typ-Zusicherung abgesichertwerden: c := s(C). Ähnlich verhält es sich auch mit Zuweisungen zwischen zweiReferenzen auf Signaturen:TYPE R = POINTER TO RD;RD = SIGNATURE END;S = POINTER TO SD;SD = SIGNATURE {RD}END;C = POINTER TO CD;CD = RECORD {SD} END;VAR r: R; s: S; c: C;Hier wären die Zuweisungen r := s und s := r(S) erlaubt, außerdem natürlich auch s:= r(C). Für Referenzen auf Klassen gelten ohnehin die in Oberon-2 üblichen Regeln.Damit sind auch die Spracherweiterungen für Signaturen abgeschlossen.25 In Abschnitt 6.2.2 haben wir diese Frage für abstrakte Klassen nur kurz behandelt. Dort war einegenauere Analyse jedoch nicht von Bedeutung, da abstrakte Klassen ja in <strong>der</strong>selben Hierarchie wiekonkrete Klassen auftreten und deswegen auch für sie die normalen Regeln von Oberon-2 gelten.


6.2 Spracherweiterungen 1436.2.4 Mehrfaches SubtypingMehrfaches Subtyping wurde in Abschnitt 6.1.4 als einfache“ Alternative für vollständige“Mehrfachvererbung motiviert. In diesem Abschnitt wollen wir die entsprechenden” ”Spracherweiterungen für <strong>Fro<strong>der</strong>on</strong>-1 formulieren. In Abschnitt 6.3.4 werden wir unsausführlich mit den Konsequenzen für den Compiler beschäftigen, die — um es vorwegzu nehmen — nicht unbeachtlich sind.Mehrfaches Subtyping kann im Rahmen <strong>der</strong> bisher vorgestellten Erweiterungen fürabstrakte Klassen und Signaturen in zwei unterschiedlichen Bedeutungen verwendet werden:• Eine Signatur S kann als Subtype von mehreren an<strong>der</strong>en Signaturen S1, ..., Sndeklariert werden.• Eine (abstrakte o<strong>der</strong> konkrete) Klasse C kann als Implementierung von mehrerenSignaturen S1, ..., Sn deklariert werden.Natürlich können dann sowohl S als auch C überall dort eingesetzt werden, wo eineReferenz <strong>der</strong> jeweiligen Signatur erwartet wird (das ist ja gerade <strong>der</strong> durch Subtypingenstehende Polymorphismus).Eine entsprechende syntaktische Erweiterung, die mehrfaches Subtyping ermöglicht,ist leicht anzugeben. Wir müssen lediglich statt eines Bezeichners in geschweiften Klammerneine Liste von Bezeichnern erlauben, etwa in <strong>der</strong> folgenden Form:Type = ...| RECORD ["(" Qualident ")"] ["{" SignatureList "}"]FieldList {";" FieldList} END| ABSTRACT ["(" Qualident ")"] ["{" SignatureList "}"]FieldList {";" FieldList} {AbstractDecl ";"} END| SIGNATURE ["{" SignatureList "}"] {AbstractDecl ";"} END| ....SignatureList = Qualident {"," Qualident}.Wie schon in Abschnitt 6.2.3 dürfen auch in einer solchen Liste nur Bezeichner auftreten,die an Signaturen gebunden sind. Weitere syntaktische Modifikationen sind nichtnotwendig.Von den in Abschnitt 6.1.4 angesprochenen Problemen, die durch Mehrfachvererbungentstehen, bleibt für mehrfaches Subtyping nur eines übrig: Wie behandeln wirNamenskonflikte zwischen Signaturen, insbeson<strong>der</strong>e solche, die nachträglich zwischenihnen auftreten können? Rufen wir uns zunächst in Erinnerung, was ein Namenskonfliktgenau ist. Unter einem Namenskonflikt verstehen wir im allgemeinen, daß identischeBezeichner innerhalb eines Sichtbarkeitbereichs mehrfach deklariert sind. Die DeklarationenVAR a, a: INTEGER o<strong>der</strong> VAR a: INTEGER; a: REAL würden zum Beispiel zuNamenskonflikten führen, da nicht zwei verschiedene Variablen an einen Bezeichner gebundenwerden können. Zu beachten ist hier allerdings, daß für jedes a ein konkretes


144 Klassen, Typen und ObjektorientierungObjekt angelegt werden müßte. Der Konflikt tritt also auf, weil <strong>der</strong> Bezeichner a für verschiedeneObjekte stehen würde. Betrachten wir nun folgendes Beispiel für mehrfachesSubtyping von Signaturen in <strong>Fro<strong>der</strong>on</strong>-1:TYPE A = POINTER TO AD;AD = SIGNATUREPROCEDURE (VAR s: AD) X (r: REAL);END;B = POINTER TO BD;BD = SIGNATUREPROCEDURE (VAR s: BD) X (a: REAL);END;C = POINTER TO CD;CD = SIGNATURE {AD, BD}(* X() is present from AD and BD *)END;Es ist offensichtlich, daß in CD die Methode X() doppelt deklariert wäre. Da Signaturenaber lediglich Signaturen von Methoden vorgeben, werden natürlich keine konkretenObjekte an den Namen X gebunden. Demnach kommt es hier nicht zu einem Namenskonfliktim obigen Sinn. In folgendem Beispiel würde allerdings sehr wohl ein Konfliktauftreten:TYPE A = POINTER TO AD;AD = SIGNATUREPROCEDURE (VAR s: AD) X(r: REAL);END;B = POINTER TO BD;BD = SIGNATUREPROCEDURE (VAR s: BD) X(a: INTEGER );END;C = POINTER TO CD;CD = SIGNATURE {AD, BD}(* X() is present from AD and BD *)END;Hier würden die beiden X zwar wie<strong>der</strong> nicht an konkrete Objekte gebunden, allerdingsstimmen ihre Typen nun nicht mehr überein. Solche Konflikte müssen wir natürlichverbieten, da sonst nicht mehr sichergestellt wäre, welche Methodensignatur durch Xgemeint ist. 26Allerdings basiert die bisherige Handhabung von Namenskonflikten auf <strong>der</strong> strukturellenÄquivalenz von Signaturen. Diese Art <strong>der</strong> Äquivalenz ist in Oberon-2 jedoch26 Die Alternative wäre natürlich ein Überladen von Methoden, wie es in C++ o<strong>der</strong> JAVA erlaubt ist.Allerdings entstehen durch (zuviele) überladene Bezeichner im allgemeinen schwerer lesbare Programme,weswegen wir uns hier nicht näher mit dieser Alternative auseinan<strong>der</strong>setzen wollen.


6.2 Spracherweiterungen 145unüblich: Sie wird — wie in Abschnitt 2.2.1 angesprochen — lediglich zwischen Prozedurenverwendet. Signaturen, die nicht in einer Subtyping-Relation zueinan<strong>der</strong> stehen,sollten aber auch dann nicht kompatibel sein, wenn sie identische Struktur haben. Dasfolgt daraus, daß sie üblicherweise unterschiedliche Abstraktionen repräsentieren, dieauch dann getrennt bleiben sollten, wenn sie ”zufällig“ strukturell äquivalent sind. Deswegenmüssen wir also auch das erste unserer Beispiele als fehlerhaft betrachten. Wennzwei Signaturen dagegen in einer Subtyping-Relation zueinan<strong>der</strong> stehen, dann dürfenidentische Methodensignaturen mehrfach vorhanden sein. Das folgende Beispiel ist alsokorrekt:TYPE Z = POINTER TO ZD;ZD = SIGNATUREPROCEDURE (VAR s: ZD) X (r: REAL);END;A = POINTER TO AD;AD = SIGNATURE {ZD}PROCEDURE (VAR s: AD) X (r: REAL);END;B = POINTER TO BD;BD = SIGNATURE {ZD}PROCEDURE (VAR s: BD) X (a: REAL);END;C = POINTER TO CD;CD = SIGNATURE {AD, BD}(* X() is present from AD and BD *)END;An diesem Beispiel erkennen wir außerdem, daß wie<strong>der</strong>holtes Subtyping zu keinerleineuen Problemen führt (ganz im Gegensatz zu wie<strong>der</strong>holter Vererbung im Sinne vonSubtyping und Subclassing).Wir merken hier noch an, daß die Kompatibilität zwischen Referenzen auf Signaturendurch mehrfaches Subtyping nicht betroffen ist: Solange wir entscheiden können, ob zweiSignaturen in einer Subtyping-Relation zueinan<strong>der</strong> stehen, gelten die üblichen Regelnvon Oberon-2 bzw. die Regeln, die wir in Abschnitt 6.2.3 nochmals diskutiert haben.


146 Klassen, Typen und Objektorientierung6.3 Implementierung6.3.1 Allgemeine Än<strong>der</strong>ungen in Scanner und ParserDie allgemeinen Än<strong>der</strong>ungen erstrecken sich (wie schon in Abschnitt 4.4.1) auf die Integration<strong>der</strong> neuen syntaktischen Elemente, also <strong>der</strong> Schlüsselwörter ABSTRACT undSIGNATURE, in den Scanner OPS.Mod sowie die entsprechenden Än<strong>der</strong>ungen in den ProzedurenType() und RecordType() des Parsers OPP.Mod. Alle <strong>weiteren</strong> Än<strong>der</strong>ungensind spezifischer Natur und werden in den folgenden Abschnitten beschrieben.6.3.2 Ein neuer Sichtbarkeitsbereich für KlassenWir wollen uns in diesem Abschnitt damit beschäftigen, wie wir die in Abschnitt 6.2.1formulierten Erweiterungen bezüglich des neuen Sichtbarkeitsbereichs für Klassen in denCompiler OP2 einbringen können. Zunächst können wir festhalten, daß durch den neuenSichtbarkeitsbereich sicherlich eine Erweiterung <strong>der</strong> Symboldateien (siehe Abschnitt3.3.4) notwendig wird, da nun gewissermaßen zwei Schnittstellen für ein Modul vorhandensind: Eine für Klienten, eine an<strong>der</strong>e für Methoden von abgeleiteten Klassen (sieheAbschnitt 6.2.1).Bevor wir uns aber diesen Erweiterungen <strong>der</strong> Symboldatei zuwenden, wollen wirzunächst unsere neue Exportmarke ”+“inParserOPP.Mod integrieren. Für die Erkennungvon Exportmarken ist die Prozedur CheckMark() zuständig. Sie gibt in Abhängigkeitdes zuletzt gelesenen Symbols einen ganzzahligen Wert zurück, <strong>der</strong> die mit diesemSymbol verbundene Sichtbarkeit beschreibt. Diese Werte sind in Form <strong>der</strong> Konstanteninternal, external, externalR (für privat, öffentlich und schreibgeschützt) deklariert,denen wir für den neuen Sichtbarkeitsbereich eine weitere Konstante externalP(für external protected) hinzufügen. Für das Symbol ”+“ erweitern wir jetzt noch dieFallunterscheidung in CheckMark() so, daß dieser neue Wert zurückgegeben wird.Da die Verwendung <strong>der</strong> neuen Exportmarke nur für Fel<strong>der</strong> von Recordtypen sowiefür typgebundene Prozeduren zulässig ist, erweitern wir die Prozeduren, in denenCheckMark() aufgerufen wird, entsprechend:• In den Prozeduren RecordType() und TProcDecl() darf die neue Exportmarkeverwendet werden.• In den Prozeduren ProcedureDeclaration() und Block() darf die neue Exportmarkenicht verwendet werden; wir müssen also eine entsprechende Fehlermeldungerzeugen, falls sie doch verwendet wird.Damit ist die erste Modifikationen des Parsers abgeschlossen, und wir wenden uns <strong>der</strong>Symboltabelle bzw. <strong>der</strong> Symboldatei zu.Im Modul OPT.Mod führen wir zwei neue Konstanten tagPtFld und tagPtTProc ein,die neue tags für Symboldateien darstellen. Durch sie werden Fel<strong>der</strong> von Recordtypenbzw. typgebundene Prozeduren, die geschützt exportiert sind, gekennzeichnet. Dannmodifizieren wir die Prozeduren Import(), OutFlds() und OutObjs() so, daß diese


6.3 Implementierung 147Anmerkung 6.2 Fehlermeldungen in OP2Ein Problem mit OP2 trat auf, als die Erweiterungen für den geschützten Export getestetwurden. Es sollten durch zwei Test-Module alle Möglichkeiten, in diesem Zusammenhangetwas falsch bzw. richtig zu machen, überprüft werden. Einige nach unsererDefinition falsche Zugriffe auf geschützte Fel<strong>der</strong> bzw. Methoden führten aber nicht zuFehlermeldungen, was zunächst den Schluß nahelegte die semantischen Prüfungen seienfehlerhaft. Als Grund stellte sich aber dann die Heuristik zur Vermeidung zu vielerFehlermeldungen heraus, die in <strong>der</strong> Prozedur OPM.Mark() verwendet wird: Nach einemFehler werden weitere Fehler innerhalb <strong>der</strong> nächsten 10 Zeichen nicht mehr gemeldet!Das Test-Modul war jedoch an den besagten Stellen so aufgebaut, daß diese Heuristikzum Tragen kam. Sie wurde sicherheitshalber entfernt, um nicht bei <strong>weiteren</strong> Tests wie<strong>der</strong>über dieses Problem zu stolpern. Abschließend bleibt festzuhalten, daß die Suchenach dem scheinbaren Fehler in <strong>der</strong> semantischen Prüfung einen halben Tag kostete.neuen tags erkannt und richtig umgesetzt werden. Damit ist sichergestellt, daß dieentsprechenden Recordfel<strong>der</strong> und Methoden für externe Module zugänglich sind.Jetzt müssen wir nur noch sicherstellen, daß Bezeichner, die geschützt exportiertwurden, auch nur in Methoden abgeleiteter Klassen verwendet werden können. Daje<strong>der</strong> Zugriff auf einen solchen Bezeichner durch die Prozedur selector() im ModulOPP.Mod verarbeitet wird, fügen wir dort eine entsprechende Prüfung ein:• Sobald ein Zugriff auf ein Recordfeld gefunden wurde, müssen wir zunächst prüfen,ob es sich um ein geschütztes Feld handelt.• Ist dies <strong>der</strong> Fall, so müssen wir weiter überprüfen, ob <strong>der</strong> Zugriff innerhalb desSichtbarkeitsbereichs einer typgebundenen Prozedur liegt und ob diese Prozeduran den richtigen Typ (also eine Erweiterung des Typs, in dem das Feld ursprünglichdeklariert wurde) gebunden ist.Wir entwickeln zwei neue Prozeduren um diese Prüfung durchzuführen:• FindOutermostProcedure() findet zu einer gegebenen Prozedur die äußerste sieumgebende Prozedur.• FindContainingRecord() bestimmt den exakten Recordtyp, in dem ein bestimmtesFeld (Datenfeld o<strong>der</strong> typgebundene Prozedur) deklariert wurde.Erstere ist notwendig, um den lokal zu einer Methode deklarierten Prozeduren ebenfallsZugriff auf die geschützten Bezeichner gewährenzukönnen. Zweitere hingegen, umprüfen zu können, ob die gerade zu übersetzende Methode an eine Erweiterung desexportierenden Typs gebunden ist. Damit sind die Erweiterungen für den geschütztenExport abgeschlossen.


148 Klassen, Typen und Objektorientierung6.3.3 Abstrakte Klassen und MethodenWir wollen im folgenden die in Abschnitt 6.2.2 vorgestellten Spracherweiterungen fürabstrakte Klassen realisieren. Dabei lassen wir vorerst die zusätzlichen Erweiterungenfür Signaturen aus den Abschnitten 6.2.3 und 6.2.4 außer acht.Zunächst müssen wir uns darüber Gedanken machen, wie wir abstrakte Klassen inOP2 repräsentieren wollen. Für Funktionen haben wir uns in Abschnitt 5.3.3 dafür entschieden,neue Modi für die Typen OPT.StrDesc und OPT.ObjDesc einzuführen. Wiewir dort gesehen haben, hat dies aber weitreichende Konsequenzen, da je<strong>der</strong> neue Modusinnerhalb des gesamten Compilers nachgeführt werden muß. Außerdem müssen wirfür abstrakte Klassen weniger umfangreiche Prüfungen durchführen. Es erscheint alsosinnvoll, anstatt eines neuen Modus lediglich ein zusätzliches Feld abstract: BOOLEANin den Typ OPT.StrDesc aufzunehmen. Offensichtlich ist dieses Feld nur für Recordtypen(also für Instanzen von OPT.StrDesc mit comp = Record) relevant. Für diese gibtes an, ob es sich um einen abstrakten Recordtyp handelt (abstract = TRUE) o<strong>der</strong>nicht(abstract = FALSE). 27 In <strong>der</strong> Prozedur OPT.NewStr(), die neue Instanzen des TypsOPT.StrDesc anlegt, setzen wir dieses Feld auf FALSE. Die Prozeduren des Parsers,die später in <strong>der</strong> Symboltabelle die Einträge für abstrakte Klassen (bzw. Recordtypen)erzeugen, setzen dieses Feld dann auf TRUE. Abstrakte Methoden kennzeichnen wir ebenfallsdurch ein neues Feld abstract: BOOLEAN, daswirdemTypOPT.ProcDesc (sieheAbschnitt 4.4.6) hinzufügen. Die Prozedur OPT.NewProc() passen wir in <strong>der</strong> gleichenWeise, wie oben für OPT.NewStr() geschil<strong>der</strong>t, an.Natürlich dürfen abstrakte Klassen und Methoden aus dem Modul, in dem sie deklariertsind, exportiert werden. Dies erfor<strong>der</strong>t aber eine Erweiterung <strong>der</strong> Symboldateien.Dazu führen wir im Modul OPT.Mod die neuen tags tagAbsRec, tagAPro, tagPtAPro undtagHdAPro ein. Ersterer kennzeichnet abstrakte Klassen (bzw. Recordtypen) selbst, dierestlichen drei unterscheiden abstrakte Methoden nach ihrem Exportstatus, geben alsoan, ob eine Methode normal, geschützt o<strong>der</strong> nicht exportiert wurde. Damit abstrakteKlassen und Methoden in den Symboldateien korrekt verarbeitet werden können, müssenwir auch in den Prozeduren OPT.Import(), OPT.OutStr() und OPT.OutObj() wie<strong>der</strong>entsprechende Än<strong>der</strong>ungen vornehmen.Jetzt können wir uns dem Parser OPP.Mod zuwenden. Hier entwickeln wir zunächstdie Prozedur AbstractType(), die sich um die Deklaration einer abstrakten Klassekümmert. Sie übernimmt also weitgehend die Aufgaben <strong>der</strong> für konkrete Klassen vorhandenenProzedur RecordType() und wird von <strong>der</strong> Prozedur TypeDecl() aufgerufen,sobald in einer Typdeklaration das Schlüsselwort ABSTRACT erkannt wurde. Der wesentlicheUnterschied zu RecordType() ist die Prüfung, ob ein hinter ABSTRACT angegebenerBasistyp ebenfalls abstrakt ist. Sollte dies nicht <strong>der</strong> Fall sein, erzeugen wir eine Fehlermeldung.Außerdem müssen wir nach <strong>der</strong> Verarbeitung von Recordfel<strong>der</strong>n auch dieVerarbeitung von abstrakten Methoden vorsehen; implementiert wird diese in <strong>der</strong> zuAbstractType() lokalen Prozedur AbstractProc(). Dieseerfüllt für abstrakte Metho-27 Im folgenden bedeuten die Ausdrücke ”abstrakter Recordtyp“ und ”abstrakte Klasse“ natürlich dasselbe:So wie konkrete Klassen durch ”konkrete“ Recordtypen realisiert werden, werden auch abstrakteKlassen durch ”abstrakte“ Recordtypen realisiert.


6.3 Implementierung 149den weitgehend die Aufgaben <strong>der</strong> vorhandenen Prozedur TProcDecl(), allerdings mit<strong>der</strong> zusätzlichen Prüfung, daß eine redefinierte Methode in <strong>der</strong> Basisklasse nicht konkretgewesen sein darf.An dieser Stelle tritt für abstrakte Methoden, die als Empfängerparameter einenZeigertyp angeben, ein Problem auf: Da wir uns in <strong>der</strong> Prozedur AbstractProc() jainnerhalb <strong>der</strong> Deklaration des Recordtyps befinden, an den dieser Zeiger gebunden ist,steht sein endgültiger Typ noch nicht fest. 28 Der Compiler würde hier also einen Fehlermelden. Um dies zu vermeiden, müssen wir nach <strong>der</strong> Verarbeitung <strong>der</strong> Recordfel<strong>der</strong>,aber vor <strong>der</strong> Verarbeitung <strong>der</strong> abstrakten Methoden, einen Fixup aller Zeigertypendurchführen, die den aktuellen Recordtyp als Basistyp angeben. Dazu führen wir eineneue globale Variable currentTypeObj: OPT.Object ein, die wir in <strong>der</strong> ProzedurBlock() auf das (nur mit dem Namen initialisierte) Objekt des zu deklarierenden Typssetzen. So können wir in AbstractType() das globale Array FwdPtr nach solchen Zeigerndurchsuchen, die als Namen ihres Basistyps den Namen unseres abstrakten Recordtypsangegeben haben. 29Wir müssen jetzt aber auch noch weitere Prüfungen implementieren, die den in Abschnitt6.2.2 angegebenen Regeln entsprechen. Zunächst wollen wir uns um den Aufrufredefinierter Methoden kümmern, <strong>der</strong> dann nicht erlaubt ist, wenn die redefinierte Methodeabstrakt war. Dazu fügen wir eine entsprechende Fallunterscheidung in die Prozedurselector() ein und erzeugen dort gegebenenfalls eine Fehlermeldung. Weiterhinmüssen wir verhin<strong>der</strong>n, daß die vordeklarierte Prozedur NEW() mit einem Zeiger, <strong>der</strong>auf eine abstrakte Klasse verweist, aufgerufen werden kann. 30 Wir fügen also auch hiereine weitere Fallunterscheidung in die Prozedur CheckDestPar() (siehe Abschnitt 5.3.3)ein, die eine Fehlermeldung erzeugt, wenn <strong>der</strong> erste Parameter von NEW() ein Zeiger aufeinen abstrakten Recordtyp ist. Schließlich dürfen von abstrakten Recordtypen ja auchdurch direkte Deklarationen keine Instanzen erzeugt werden. Ein abstrakter Recordtypdarf also we<strong>der</strong> Typ einer Variablen noch Typ eines Recordfelds, noch Elementtyp einesArrays sein. Die hierzu notwendige Prüfung integrieren wir in die Prozedur Type().Dadurch werden alle obengenannten Fälle abgedeckt, da die Prozedur Type() für alldiese Deklarationen verwendet wird. Eine Prüfung, um sicherzustellen, daß abstrakteMethoden nicht in <strong>der</strong>selben Klasse durch konkrete Methoden überschrieben werden,müssen wir nicht extra durchführen. Sie wird schon durch die vorhandene Regel, diemehrfach deklarierte Bezeichner in einem Sichtbarkeitsbereich (hier in dem des Records)verbietet, realisiert.28 In OP2 wird <strong>der</strong> Typ eines Zeigers, <strong>der</strong> auf einen noch nicht deklarierten Recordtyp verweist,zunächst auf undefined gesetzt. Nach je<strong>der</strong> Deklaration eines Recordtyps wird dann eine Liste allernoch undefinierten Zeiger durchsucht und gegebenenfalls <strong>der</strong> gerade erkannte Recordtyp als Basistypdes jeweiligen Zeigers eingetragen.29 Wir kommen hier mit einer globalen Variable aus, da die Deklaration einer abstrakten Klasse nichtgeschachtelt vorkommen kann. Eine abstrakte Klasse lokal zu einer Prozedur zu deklarieren, würde auchdeswegen keinen Sinn machen, da sie nie durch eine konkrete Klasse (<strong>der</strong>en Methoden global deklariertwerden müßten) implementiert werden könnte.30 Wie auch in Abschnitt 5.3.3 verhin<strong>der</strong>n wir einen Aufruf <strong>der</strong> Prozedur SYSTEM.NEW() nicht, da essich um eine Operation auf niedrigster Ebene handelt, für die <strong>der</strong> Programmierer die Verantwortungübernehmen muß.


150 Klassen, Typen und ObjektorientierungSchließlich müssen wir noch eine letzte Prüfung durchführen, <strong>der</strong>en Komplexitätzunächst deutlich über <strong>der</strong> <strong>der</strong> an<strong>der</strong>en Prüfungen zu liegen scheint: Wir müssen feststellen,ob in einer konkreten Klasse auch wirklich alle abstrakten Methoden, die inihren abstrakten Basisklassen deklariert sind, redefiniert wurden. Dies können wir aberaufgrund <strong>der</strong> syntaktischen Trennung von konkreten Klassen (Recordtypen) und konkretenMethoden (typgebundenen Prozeduren) nicht lokal (an einer Stelle) im Parserüberprüfen: Während <strong>der</strong> Übersetzung könnte ja je<strong>der</strong>zeit“ noch eine weitere konkrete”Methode deklariert werden. Wir müssen diese Prüfung also zwangsläufig durchführen,wenn das zu übersetzende Modul den Parser durchlaufen hat und damit alle konkretenMethoden je<strong>der</strong> konkreten Klasse bekannt sind. Wie können wir die Prüfung aber dannrealisieren?Eine offensichtliche Möglichkeit wäre ein nachträgliches Traversieren aller Recordtypenin<strong>der</strong>Symboltabelle.Dabeikönntenwir für die Prüfung sowohl abstrakte Klassenals auch konkrete Klassen, die von einer an<strong>der</strong>en konkreten Klasse abgeleitet wurden,außer acht lassen. 31 Für jede verbleibende konkrete Klasse würden wir dann die Ketteihrer Basisklassen traversieren und dabei alle in ihr deklarierten Methoden in einer geeignetenDatenstruktur ablegen. Diese Menge von Methoden“ partitionieren wir dann in”zwei disjunkte Teilmengen von abstrakten und konkreten Methoden. 32 Jetzt überprüfenwir, ob für jede abstrakte Methode auch eine passende konkrete Methode vorhanden ist.Ist dies nicht <strong>der</strong> Fall, müssen wir eine Fehlermeldung erzeugen.Diese Lösung ist offensichtlich korrekt, und <strong>der</strong> Mathematiker in uns“ wäre mit ihr”auch zufrieden (die Existenz einer Lösung würde ja ausreichen). Allerdings wäre eineImplementierung in dieser Form nicht son<strong>der</strong>lich effizient, was den Pragmatiker in uns“”natürlich herausfor<strong>der</strong>t, nach einer an<strong>der</strong>en Lösung zu suchen.Die grundlegende Idee für eine effizientere Lösung ist es, alle schon im Parser zurVerfügung stehenden Informationen zu nutzen, um die Prüfung selbst zu vereinfachen.Zum einen können wir für abstrakte Klassen lokal (in <strong>der</strong> Prozedur AbstractProc())feststellen, ob durch eine abstrakte Methode lediglich eine an<strong>der</strong>e redefiniert wurde o<strong>der</strong>ob sie eine neue“ Methode darstellt. Damit läßt sich die Anzahl <strong>der</strong> abstrakten Methodeneiner abstrakten Klasse bestimmen. Zum an<strong>der</strong>en können wir für konkrete Klassen”ebenfalls lokal feststellen (in <strong>der</strong> Prozedur TProcDecl()), ob durch eine konkrete Methodeeine bis jetzt abstrakte Methode redefiniert wurde. Damit läßt sich die Anzahl<strong>der</strong> noch abstrakten Methoden“ in einer konkreten Klasse bestimmen. Zu überprüfen”bleibt dann lediglich, ob für jede konkrete Klasse gilt, daß die Anzahl <strong>der</strong> abstraktenMethoden in ihr gleich 0 ist. Diese Prüfung können wir als Teil <strong>der</strong> ohnehin notwendigenTraversierung <strong>der</strong> Symboltabelle in die Prozedur OPV.Traverse() des Backendseinfügen. Im einzelnen müssen wir also die folgenden Än<strong>der</strong>ungen an OP2 vornehmen:31 Zum einen betrifft die Prüfung ja nur konkrete Klassen, zum an<strong>der</strong>en müssen schon in <strong>der</strong> ersten“ ”konkreten Klasse alle abstrakten Methoden redefiniert worden sein.32 Wir könnten natürlich auch nur diese zwei Mengen aufbauen und uns damit den ersten Schrittsparen.


6.3 Implementierung 151• Wir führen im Typ OPT.StrDesc ein neues Feld nofAbstractMethods: INTEGERein. In <strong>der</strong> Prozedur OPT.NewStr() setzen wir dieses Feld auf 0.• In den Prozeduren OPP.RecordType() und OPP.AbstractType() initialisieren wirdieses Feld mit <strong>der</strong> Anzahl <strong>der</strong> abstrakten Methoden des Basistyps (sofern einsolcher angegeben wurde).• In <strong>der</strong> Prozedur OPP.AbstractProc() erhöhen wir die Anzahl <strong>der</strong> abstrakten Methodenum 1, wenn wirklich eine neue abstrakte Methode deklariert wurde. Wenndagegen lediglich eine an<strong>der</strong>e abstrakte Methode überschrieben wurde, bleibt <strong>der</strong>alte Wert erhalten.• In <strong>der</strong> Prozedur OPP.TProcDecl() verringern wir die Anzahl <strong>der</strong> abstrakten Methodenum 1, wenn eine abstrakte Methode <strong>der</strong> Basisklasse durch eine konkreteMethode redefiniert wurde.• In <strong>der</strong> Prozedur OPV.TraverseRecord() überprüfen wir für jede konkrete Klasse,ob nofAbstractMethods = 0 gilt. Ist dies nicht <strong>der</strong> Fall, erzeugen wir eineFehlermeldung.Diese Lösung ist offensichtlich effizienter, allerdings müssen wir jetzt die Anzahl <strong>der</strong>abstrakten Methoden einer abstrakten Klasse explizit in die Symboldatei aufnehmen.Dazu definieren wir einen <strong>weiteren</strong> tag tagNApro (vergleichbar mit dem vorhandenentag tagNTPro) und ergänzen die Prozeduren OPT.Import() und OPT.OutStr() entsprechend.Damit ist die Implementierung <strong>der</strong> abstrakten Klassen und Methoden vorerstabgeschlossen.6.3.4 Signaturen und Mehrfaches SubtypingIn diesem Abschnitt beschäftigen wir uns mit <strong>der</strong> Implementierung <strong>der</strong> in Abschnitt 6.2.3und Abschnitt 6.2.4 vorgestellten Spracherweiterungen für Signaturen und mehrfachesSubtyping. Viele <strong>der</strong> dazu an OP2 notwendigen Modifikationen haben starke Ähnlichkeitmit den in Abschnitt 6.3.3 für abstrakte Klassen beschriebenen und werden im folgendennur relativ kurz erläutert. Die für Signaturen und mehrfaches Subtyping zusätzlichnotwendigen Än<strong>der</strong>ungen erörtern wir dagegen ausführlich. Dort werden wir auch aufeinige — im Rahmen dieser Arbeit — nicht lösbare Probleme stoßen.Modifikationen mit Ähnlichkeit zu abstrakten KlassenWie wir schon in Abschnitt 6.2.3 bemerkt haben, sind viele <strong>der</strong> Regeln für abstrakteKlassen auch für Signaturen relevant. Diese Ähnlichkeit ermöglicht es uns, viele <strong>der</strong> inAbschnitt 6.3.3 erarbeiteten Vorgehensweisen auf Signaturen zu übertragen.Wir repräsentieren Signaturen und in ihnen deklarierte Methoden in OP2 durchzwei neue Fel<strong>der</strong> signature: BOOLEAN in den Typen OPT.StrDesc und OPT.ProcDesc.Diese werden genau wie die entsprechenden Fel<strong>der</strong> für abstrakte Klassen gehandhabt:Die Prozeduren OPT.NewStr() und OPT.NewProc() setzen sie zunächst auf FALSE. Die


152 Klassen, Typen und ObjektorientierungProzeduren des Parsers, in denen Signaturen später verarbeitet werden, setzen sie entsprechendauf TRUE. 33Da auch Signaturen aus dem Modul, in dem sie deklariert sind, exportiert werdenkönnen, müssen wir in den Symboldateien ebenfalls Erweiterungen vornehmen. Wirführen im Modul OPT.Mod die neuen tags tagSigRec, tagSPro und tagHdSPro ein. Erstererkennzeichnet die Signatur selbst, die beiden an<strong>der</strong>en unterscheiden die Methodeneiner Signatur nach ihrem Exportstatus. Auffallend ist hier, daß wir für Signaturen(im Gegensatz zu abstrakten Klassen in Abschnitt 6.3.3) den geschützten Export vonMethoden nicht unterstützen. Warum dies so ist, liegt auf <strong>der</strong> Hand: Der geschützteExport dient zur besseren Wie<strong>der</strong>verwendbarkeit <strong>der</strong> Implementierung von Klassen. DaSignaturen aber lediglich Schnittstellen von Klassen sind und damit insbeson<strong>der</strong>e keineImplementierung haben, macht es auch keinen Sinn, diesen Mechanismus für sie zuerlauben. Um unsere neuen tags verwenden zu können, müssen wir natürlich auch inden Prozeduren OPT.Import(), OPT.OutStr() und OPT.OutObj() wie<strong>der</strong> entsprechendeÄn<strong>der</strong>ungen vornehmen.Die im Parser für die Verarbeitung von Signaturen notwendigen Modifikationen entsprechendweitgehend denen für abstrakte Klassen. Wir entwickeln zunächst die ProzedurSignatureType(), die sich um die Deklaration einer Signatur kümmert und vonTypeDecl() aufgerufen wird, sobald das Schlüsselwort SIGNATURE erkannt wurde. Dain Signaturen keine Datenfel<strong>der</strong> deklariert werden können, vereinfacht sich diese Prozedurim Vergleich zu AbstractType() und RecordType() erheblich. Zusätzlich müssenwir sicherstellen, daß eine Signatur nur von an<strong>der</strong>en Signaturen abgeleitet wurde. Auchin <strong>der</strong> Prozedur TProcDecl() müssen wir eine neue Prüfung durchführen, da konkreteMethoden nicht an Signaturen gebunden werden dürfen. Die in Signaturen deklariertenMethoden werden durch die lokale Prozedur SignatureProc() verarbeitet. Hier entfälltim Gegensatz zu AbstractProc() die Prüfung, ob eine abstrakte Methode eine konkreteMethode überschreiben würde (wie durch obige Prüfung sichergestellt, können in Signaturenja keine konkreten Methoden deklariert werden). Das in Abschnitt 6.3.3 angesprocheneProblem für Empfängerparameter eines Zeigertyps lösen wir wie<strong>der</strong> durch einenvorgezogenen Fixup aller noch undefinierten Zeigertypen. Die <strong>weiteren</strong> in Abschnitt6.3.3 beschriebenen Prüfungen (für Aufrufe redefinierter Methoden, die Anwendung vonNEW() und die explizite Deklaration von Variablen) lassen sich direkt auf Signaturenübertragen. Wie dort für abstrakte Klassen gezeigt, führen wir also in den Prozedurenselector(), CheckDestPar() und Type() neue Fallunterscheidungen für Signaturenein.Die in Abschnitt 6.3.3 besprochene Prüfung, mit <strong>der</strong> wir sicherstellen, daß in konkretenKlassen alle abstrakten Methoden einer abstrakten Basisklasse redefiniert wurden,ist in abgewandelter Form auch für Signaturen notwendig. Hier müssen wir jedoch konkreteund abstrakte Klassen dahingehend überprüfen, ob alle Methoden einer Signatur,die sie implementieren wollen, redefiniert wurden. Hier wird klar, warum wir Signatu-33 Daß wir für Signaturen neue Fel<strong>der</strong> einführen, scheint zunächst nicht erfor<strong>der</strong>lich zu sein: Wirkönnten ja auch die Fel<strong>der</strong> für abstrakte Klassen wie<strong>der</strong>verwenden. Allerdings wäre es uns dann nichtmöglich, Signaturen und abstrakte Klassen zu unterscheiden. Weiter unten wird aber offensichtlich, daßdies zwingend erfor<strong>der</strong>lich ist.


6.3 Implementierung 153ren trotz ihrer Ähnlichkeit zu abstrakten Klassen differenziert behandeln müssen: Damitwir diese Prüfung korrekt durchführen können, ist es notwendig, zwischen abstraktenKlassen und Signaturen unterscheiden zu können. <strong>Zur</strong> Implementierung dieser Prüfungverwenden wir wie<strong>der</strong> das in Abschnitt 6.3.3 für abstrakte Klassen vorgestellte Verfahren:Wir ”zählen“ die Anzahl <strong>der</strong> Methoden in Signaturen und for<strong>der</strong>n von (abstraktenund konkreten) Klassen, daß diese Anzahl gleich 0 ist. In den Typ OPT.StrDesc führenwir dazu das Feld nofSignatureMethods: INTEGER ein, das wir wie folgt verwalten:• In den Prozeduren SignatureType() bzw. SignatureProc() zählen wir nach demVerfahren aus Abschnitt 6.3.3 die Anzahl <strong>der</strong> Methoden einer Signatur.• In den Prozeduren AbstractType() und AbstractProc() bzw. RecordType()und TProcDecl() verringern wir entsprechend die Zahl <strong>der</strong> noch vorhandenen“”Methoden aus Signaturen, wenn eine solche Methode redefiniert wurde.• In <strong>der</strong> Prozedur OPV.Traverse() führen wir jetzt zusätzlich noch die entsprechendenPrüfungen durch, die für (abstrakte und konkrete) Klassen sicherstellen, daßnofSignatureMethods = 0 gilt.Natürlich müssen wir jetzt auch für Signaturen die Anzahl <strong>der</strong> in ihnen deklariertenMethoden in die Symboldatei aufnehmen. Dazu führen wir im Modul OPT.Mod denneuen tag tagNSPro ein, den wir in den Prozeduren Import() und OutStr() behandeln.Damit haben wir die Modifikationen, die sich mit den entsprechenden Modifikationenaus Abschnitt 6.3.3 vergleichen lassen, durchgeführt.Exkurs: Dynamisches Binden fürKlasseninOberon-2Damit Signaturen sinnvoll eingesetzt werden können, müssen wir es erlauben, daß übereinen Zeiger, <strong>der</strong> statisch an eine Signatur gebunden ist, dynamisch Methoden einerkonkreten Klasse aufgerufen werden können. Um die dazu notwendigen Erweiterungenentwickeln zu können, beschäftigen wir uns in diesem Abschnitt zunächst mit den in OP2(und allen an<strong>der</strong>en uns bekannten Oberon-2-Compilern) verwendeten Mechanismen zurdynamischen Bindung von Aufrufen an Methoden. Dabei müssen wir teilweise auch aufdas Oberon-System selbst eingehen, da dort (jedenfalls für OP2) die Verwaltung <strong>der</strong> zurLaufzeit notwendigen Informationen erledigt wird.Unsere Beschreibung des dynamischen Bindens in Oberon-2 orientiert sich an dem inListing 6.6 gezeigten Modul Points. Das Modul deklariert eine Klasse Point, die zweiMethoden SetCoords() und GetCoords() anbietet. In dem Kommando Do() werdenzwei Instanzen dieser Klasse angelegt und mit den angegebenen Koordinaten initialisiert.Für den Typ Point existiert zur Laufzeit ein sogenannter Typdeskriptor, <strong>der</strong> imwesentlichen die folgenden Informationen enthält:• Verweise auf die Typdeskriptoren aller Basistypen von Point. Diese werden zurRealisierung von Typ-Zusicherungen und Typ-Prüfungen (siehe Abschnitt 2.4.2)verwendet.


154 Klassen, Typen und ObjektorientierungListing 6.6 Beispiel für dynamisches Binden in Oberon-2MODULE Points;TYPE Point = POINTER TO PointDesc;PointDesc = RECORDx, y: INTEGER;next: Point;END;PROCEDURE (self: Point) SetCoords (x, y: INTEGER);BEGINself.x := x; self.y := y;END SetCoords;PROCEDURE (self: Point) GetCoords (VAR x, y: INTEGER);BEGINx := self.x; y := self.y;END GetCoords;PROCEDURE Do* (); (* command *)VAR a, b: Point;BEGINNEW (a); NEW (b);a.SetCoords (10, 10);b.SetCoords (20, 20);END Do;END Points.• Die Offsets aller in Point deklarierten Zeiger. Diese werden vom garbage collectorverwendet, um alle erreichbaren Objekte im heap zu markieren.• Die Adressen aller an diesen Typ gebundenen Methoden. Diese werden zur Realisierungdes dynamischen Bindens benötigt.Um den Deskriptor einer Instanz des Typs Point zu finden, wird die Deklaration desTyps durch den Compiler wie folgt erweitert: 34PointDesc = RECORDtag: Type;x, y: INTEGER;next: Point;END;Vor den eigentlichen Nutzdaten wird also ein ”versteckter Zeiger“ auf den Typdeskriptoreingefügt. Der Typdeskriptor selbst wird durch das Oberon-System angelegt, wenn das34 Dies ist natürlich eine vereinfachte Sichtweise, die für das Folgende aber ausreichend exakt ist. Auchdie <strong>weiteren</strong> Erläuterungen versuchen nicht, Details des Mechanismus zu erklären, son<strong>der</strong>n vielmehrseine Essenz.


6.3 Implementierung 155Modul Points erstmals geladen wird. Der in je<strong>der</strong> Instanz des Typs Point versteckteZeiger wird durch die Standardprozedur NEW() so initialisiert, daß er auf diesen Typdeskriptorverweist. Den Typdeskriptor selbst können wir uns als Instanz des folgendenPseudo-Recordtyps“ vorstellen:35”Type = POINTER TO TypeDesc;TypeDesc = RECORDmethods: ARRAY methodCnt OF SYSTEM.ADDRESS;size: LONGINT;extensionLevel: INTEGER;methodCnt: INTEGER;module: Module;reserved: LONGINT;name: ARRAY 24 OF CHAR;baseTypes: ARRAY 8 OF Type;pointerOffsets: ARRAY numPointers OF LONGINTENDFür unseren Typ Point würde also methodCnt = 2 gelten, da zwei Methoden an diesenTyp gebunden sind. Es ist klar, daß <strong>der</strong> Compiler den einzelnen Methoden eindeutigeIndizes im Feld methods zuordnen muß, damit er den Code für einen Aufruf generierenkann. Der Aufruf a.SetCoords (10, 10) würde also wie folgt ablaufen:a.tag.methods[offsetSetCoords](a, 10, 10)Der Empfängerparameter a wird intern als ”normaler“ Parameter behandelt und deshalbhier ebenfalls an die Methode übergeben.Wenn wir in einer von Point abgeleiteten Klasse die Methode SetCoords() redefinierenwürden, so würde sie auch dort den Index offsetSetCoords erhalten. Damit wäre<strong>der</strong> obige Aufruf auch für Instanzen des abgeleiteten Typs korrekt. Jetzt würde allerdings<strong>der</strong> Typdeskriptor <strong>der</strong> abgeleiteten Klasse verwendet und damit die an diesen Typgebundene Prozedur SetCoords() aufgerufen. Die Adressen von Methoden, die nichtüberschrieben wurden, werden aus dem Typdeskriptor <strong>der</strong> Basisklasse kopiert. Wennin abgeleiteten Typen neue Methoden eingeführt werden, erhalten diese logischerweisegrößere Indizes. 3635 Explizit wird <strong>der</strong> Typ eines Typdeskriptors nie deklariert. Die entsprechenden Prozeduren desCompilers arbeiten mit Offsets, die wir uns relativ zu dem Feld size vorstellen können (Methodenwerden also über negative Offsets adressiert). Eine ähnliche Deklaration finden wir im Modul Types.Moddes Oberon-Systems. Dort wird sie aber nur für Zwecke <strong>der</strong> Metaprogrammierung eingesetzt.36 Ein offensichtlicher Nachteil dieser Methode besteht allerdings darin, daß die Anzahl <strong>der</strong> Methodeneiner Klasse in die Symboldatei mit aufgenommen werden muß. Eine neu eingefügte Methode kann alsoauch dann Klienten invalidieren, wenn sie nicht exportiert wurde (siehe auch [28] und [11]).


156 Klassen, Typen und ObjektorientierungDas Problem: Dynamisches Binden für SignaturenWas än<strong>der</strong>t sich nun an <strong>der</strong> oben geschil<strong>der</strong>ten Vorgehensweise wenn wir Signaturenunterstützen wollen? Betrachten wir dazu das in Listing 6.7 gezeigte Beispiel.Listing 6.7 Beispiel für dynamisches Binden in <strong>Fro<strong>der</strong>on</strong>-1MODULE Example;IMPORT M := SomeModule;TYPE A = POINTER TO AD;AD = SIGNATUREPROCEDURE (self: A) X ();END;C = POINTER TO CD;CD = RECORD (M.T) {AD}END;PROCEDURE (self: C) X ();BEGIN (* ... *)END X;PROCEDURE Do* (); (* command *)VAR a: A; c: C;BEGINNEW (c);c.X ();a := c;a.X ();END Do;END Example.Das Kommando Do() legt hier eine Instanz <strong>der</strong> Klasse C an und ruft dann die MethodeX() auf. Die Übersetzung von C sowie dieser erste Aufruf kann wie oben besprochenrealisiert werden: Die Methoden <strong>der</strong> Basisklasse M.T sind beim Import des Moduls Mbekannt, <strong>der</strong> Compiler kann also für die neue Methode X() einen neuen Index vergeben.Ein Problem tritt aber für die Signatur A auf. Bei ihrer Übersetzung ist nicht bekannt,ob es außer X() in konkreten Klassen auch noch an<strong>der</strong>e Methoden gibt. Hiermüssen wir X() also einen Index, <strong>der</strong> nur innerhalb <strong>der</strong> Signatur gilt, geben. Dieser mußdann allerdings zur Laufzeit auf den Index <strong>der</strong> konkreten Methode X() in C abgebildetwerden.Um diese Abbildung zu realisieren, wäre es nötig, auch für A einen eigenen ”Typdeskriptor“anzulegen. Ein solcher ”Typdeskriptor“ würde durch ein Array folgen<strong>der</strong>Form repräsentiert:SignatureDesc = ARRAY methodCnt OF LONGINT;Da in verschiedenen konkreten Klassen die Methode X() an verschiedenen Indizes liegenkann, müßten wir einen solchen Signaturdeskriptor also für jedes Paar <strong>der</strong> Form (Signa-


6.3 Implementierung 157tur, Klasse) anlegen. Nach <strong>der</strong> Zuweisung a := c müßte für den Aufruf a.X() also <strong>der</strong>Signaturdeskriptor (A, C) verwendet werden. <strong>Zur</strong> Realisierung des Signaturdeskriptorshätten wir verschiedene Möglichkeiten:• Wir könnten den Signaturdeskriptor in den Typdeskriptor <strong>der</strong> konkreten Klasseeinbetten.• Wir könnten vor je<strong>der</strong> Instanz eines Records nicht nur einen Zeiger auf den Typdeskriptor,son<strong>der</strong>n auch einen Zeiger auf den eventuell notwendigen Signaturdeskriptoraufnehmen.• Wir könnten einen Zeiger, <strong>der</strong> an eine Signatur gebunden ist, durch ein Paar vonzwei Zeigern repräsentieren. Der erste Zeiger würde wie bis jetzt als Ziel vonZuweisungen fungieren, <strong>der</strong> zweite würde stets auf den zum dynamischen Typ deserste Zeigers passenden Signaturdeskriptor verweisen.Wir wollen hier nicht genauer auf die einzelnen Vor- und Nachteile dieser Lösungeneingehen, da (wie in obigen Erläuterungen deutlich wird) für jede Lösung zur Laufzeiteine neue Art von Deskriptoren nötig wäre. Dies würde dazu führen, daß wir im Oberon-System selbst einschneidende Verän<strong>der</strong>ungen vornehmen müßten. Im Rahmen dieserArbeit wurden aber nur Än<strong>der</strong>ungen am Compiler OP2 selbst betrachtet, nicht zuletztum den Umfang <strong>der</strong> Arbeit überschaubar zu halten. Deswegen müssen hier wir voneiner Implementierung von Signaturen — und damit auch von mehrfachem Subtyping— absehen.ZusammenfassungZusammenfassend war es uns nicht möglich, die Erweiterungen für Signaturen und mehrfachesSubtyping im Rahmen dieser Arbeit vollständig zu implementieren. Das zentraleProblem sind die für Oberon-2 zur Laufzeit notwendigen Typinformationen, die sich nurdann in OP2 integrieren lassen würden, wenn wir auch weite Teile des Oberon-Systemsmiteinbeziehen.Wie schon in Abschnitt 4.4.7 betont sollten diese Probleme bei <strong>der</strong> Realisierungjedoch nicht als ”Absage“ an die eingeführten Konzepte mißverstanden werden. Auchhier sind wir nach wie vor von ihrer Berechtigung, ja sogar ihrer Notwendigkeit für die<strong>Entwicklung</strong> flexibler Software-Systeme überzeugt. Im Rahmen von <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong>soll weiter an ihrer Umsetzung gearbeitet werden, vor allem durch die <strong>Entwicklung</strong> einesneuen Compilers, <strong>der</strong> — im positiven Sinne — frei von Altlasten ist, in den wir alsoohne weitere Probleme auch Än<strong>der</strong>ungen wie die hier notwendigen einbringen können.


Kapitel 7Zusammenfassung und AusblickFrom without, as Schopenhauer noted, the new idea is firstdenounced as the work of the insane, in a few years it isconsi<strong>der</strong>ed obvious and mundane, and finally the originaldenouncers will claim to have invented it.— Alan C. Kay, zitiert nach [52]In diesem abschließenden Kapitel fassen wir die Ergebnisse dieser Arbeit zusammen.Wir bewerten außerdem den Nutzen <strong>der</strong> neuen Konzepte von <strong>Fro<strong>der</strong>on</strong>-1 und den zuihrer Realisierung nötigen Aufwand im Vergleich zu Oberon-2. Weiterhin gehen wir aufverwandte Ansätze in an<strong>der</strong>en Programmiersprachen ein und versuchen, einen Ausblickauf die weitere <strong>Entwicklung</strong> von Oberon-2 zu geben.


160 Zusammenfassung und Ausblick7.1 Ergebnisse dieser ArbeitIn diesem Abschnitt wollen wir die einzelnen in dieser Arbeit untersuchten Erweiterungenzusammenfassend darstellen und beurteilen. Dabei zählen wir nochmals Vor- undNachteile je<strong>der</strong> Erweiterung auf, geben ihre Konsequenzen für das Laufzeitverhalten anund diskutieren den zu ihrer Realisierung nötigen Aufwand.Formale SpezifikationDie Erweiterungen zur Spezifikation von Oberon-2-Programmen durch Vor- und Nachbedingungenfür Prozeduren, Invarianten und Varianten für Schleifen sowie Invariantenfür Klassen und Module ermöglichen eine explizite Beschreibung <strong>der</strong> Semantik <strong>der</strong>entsprechenden Konstrukte. Ihre syntaktische Integration bringt im Vergleich zu <strong>der</strong>vordeklarierten Standardprozedur ASSERT() eine Reihe von Vorteilen:• Dem Anwen<strong>der</strong> <strong>der</strong> Programmiersprache wird die Verwendung dieser Mechanismendeutlicher nahegelegt, auch wenn wir sie aus pragmatischen Gründen nichterzwingen können.• Für die Dokumentation von Korrektheitsbeweisen können wir auf Kommentareverzichten, die notwendig wären, um auf die Bedeutung eines Aufrufs von ASSERT()hinzuweisen ( ”Handelt es sich um eine Vorbedingung o<strong>der</strong> eine Invariante?“).• Die Spezifikation <strong>der</strong> exportierten Teile eines Moduls erleichtert sowohl dem Entwicklereines Moduls als auch dem Entwickler von Klienten ihre jeweiligen Tätigkeiten,da die Zuständigkeiten klar abgegrenzt werden können (VertraglichesProgrammieren).Als nachteilig können wir dagegen lediglich die erhöhte Komplexität <strong>der</strong> Sprache selbstanführen sowie den — aus pragmatischen Gründen notwendigen — Verzicht auf Elemente<strong>der</strong> Prädikatenlogik (Quantoren).Die Verwendung von Zusicherungen hat durch die zusätzlich notwendigen PrüfungenAuswirkungen auf die Laufzeit eines Programms. In gewissem Rahmen können dieseEffizienzverluste aber durch neue Möglichkeiten zur Optimierung wie<strong>der</strong> aufgehobenwerden. Darüber hinaus stellt sich natürlich die Frage, ob es wichtiger ist, ein Programmso effizient wie möglich o<strong>der</strong> es so sicher wie möglich zu machen.Die vorgestellten Spracherweiterungen konnten allerdings in dieser Arbeit nicht vollständigrealisiert werden. Hierfür gibt es im wesentlichen zwei Gründe:• Invarianten für Klassen würden einen expliziten Mechanismus voraussetzen, umeinzelne Instanzen direkt nach ihrer Erzeugung in einen zur Invariante konformenZustand zu bringen. Da Oberon-2 aber keinen solchen Mechanismus (wie zumBeispiel C++ o<strong>der</strong> Eiffel in <strong>der</strong> Form von Konstruktoren) anbietet, können wirdies nicht garantieren.


7.1 Ergebnisse dieser Arbeit 161• Vor- und Nachbedingungen für Methoden würden voraussetzen, daß wir konforme— also mit <strong>der</strong> Subtyping-Relation verträgliche, das heißt in <strong>der</strong> richtigen Weisestärkere bzw. schwächere — Vor- und Nachbedingungen automatisch konstruierenkönnten. Hierzu wäre <strong>der</strong> Zugriff auf Teile <strong>der</strong> Syntaxbäume externer Modulenotwendig, <strong>der</strong> in OP2 aber nicht ohne weiteres möglich ist.Beide Probleme sollten in <strong>weiteren</strong> Arbeiten untersucht werden, um nach ihrer Lösungeine vollständige Spezifikation von Modulen und Programmen zu ermöglichen, soweit siebei Verzicht auf Prädikatenlogik möglich ist.Funktionen und ParameterübergabeAuch konstante Prozedurparameter und seiteneffektfreie Funktionen tragen zu einer genauerenSpezifikation von Modulen und Programmen bei. Daneben können wir abernoch weitere Vorteile angeben:• Sie erleichtern die Durchführung von Korrektheitsbeweisen, da die Möglichkeitenzur Erzeugung von Seiteneffekten explizit (und durch den Compiler verifizierbar)eingeschränkt sind.• Durch konstante Prozedurparameter wird vermieden, daß <strong>der</strong> Anwen<strong>der</strong> sich zwischenSicherheit (was die Einhaltung <strong>der</strong> Spezifikation einer Prozedur angeht) undEffizienz (<strong>der</strong> Parameterübergabe) entscheiden muß.• Konstante Prozedurparameter ermöglichen eine bessere Analyse von Aliasing-Effekten,da sie den über einen Parameter stattfindenden Datenfluß explizit machen.• Seiteneffektfreie Funktionen schaffen neue Möglichkeiten zur Optimierung, da inAusdrücken, die nur Aufrufe solcher Funktionen enthalten, Seiteneffekte ausgeschlossenwerden können. Die Reihenfolge, in <strong>der</strong> die Auswertung eines Ausdruckserfolgt, wird damit irrelevant.Auch hier ist lediglich die erhöhte Komplexität <strong>der</strong> Sprache selbst ein Kritikpunkt. Dieseerhöhte Komplexität steht allerdings in guter Relation zu den durch diese Erweiterungenmöglichen Verbesserungen.Sowohl konstante Prozedurparameter als auch seiteneffektfreie Funktionen habenkeine Auswirkung auf das Laufzeitverhalten von Programmen. Der für sie erzeugteMaschinencode entspricht dem, <strong>der</strong> auch sonst für Prozeduren und die Übergabe vonParametern erzeugt wird.Der Realisierungsaufwand hält sich ebenfalls für beide Erweiterungen in Grenzen.Für konstante Prozedurparameter können die für den schreibgeschützten Export vonBezeichnern ohnehin vorhandenen Mechanismen von OP2 wie<strong>der</strong>verwendet werden, undum sicherzustellen, daß Funktionen keine Seiteneffekte haben, sind ebenfalls nur kleinereÄn<strong>der</strong>ungen notwendig. Darüber hinaus betreffen diese Än<strong>der</strong>ungen zum großen Teil nurdas Frontend des Compilers, sie können also leicht auf an<strong>der</strong>e Plattformen übertragenwerden.


162 Zusammenfassung und AusblickObjektorientierungDie umfangreichsten Erweiterungen haben wir in Oberon-2 zur Unterstützung <strong>der</strong> objektorientiertenProgrammierung eingebracht. Der geschützte Export und <strong>der</strong> dadurchentstehende geschützte Sichtbarkeitsbereich sowie explizit gekennzeichnete abstrakteKlassen und Methoden sind hauptsächlich für die Implementierung objektorientierterArchitekturen von Bedeutung:• Der geschützte Export ermöglicht es, klar zwischen privaten, öffentlichen, und zurWie<strong>der</strong>verwendung sinnvollen Elementen einer Klasse zu unterscheiden. So wirdvermieden, daß <strong>der</strong> Anwen<strong>der</strong> <strong>der</strong> Sprache für durch Subclassing wie<strong>der</strong>verwendbareKomponenten gezwungen ist, sich zwischen Effizienz (öffentlicher Export vonDatenfel<strong>der</strong>n) und Sicherheit (Zugriffsprozeduren) zu entscheiden. 1• Abstrakte Klassen und Methoden erlauben es, die Implementierung gewisser Operationeneine Klasse offenzuhalten und damit ihre Allgemeinheit sicherzustellen.Darüber hinaus können wir durch eine explizite Kennzeichnung abstrakter Klassenviele Fehler statisch vermeiden, die bei ihrer Anwendung als Entwurfsmusterdynamisch geprüft werden müßten.Das Laufzeitverhalten eines Programms wird durch diese Erweiterungen nicht beeinflußt,im Fall des geschützten Exports kommt es schlimmstenfalls“ zu Verbesserungen.”Der Realisierungsaufwand ist jedoch für beide Erweiterungen sehr unterschiedlich.Für den geschützten Export sind verschwindend wenige Än<strong>der</strong>ungen nötig. Die Unterstützungabstrakter Klassen und Methoden erfor<strong>der</strong>t hingegen relativ viele Än<strong>der</strong>ungenim Frontend und sogar einige in Teilen des Backends (allerdings nicht in <strong>der</strong>Codegenerierung selbst).Für Signaturen und mehrfaches Subtyping steht hingegen wie<strong>der</strong> <strong>der</strong> Aspekt <strong>der</strong>Spezifikation im Vor<strong>der</strong>grund:• Durch Signaturen und Klassen können Spezifikation und Implementierung einesabstrakten Datentyps getrennt werden, was allgemein als Vorteil anerkannt wird.• Die weitere Trennung von Subtyping und Subclassing ermöglicht es uns, Subclassingals reinen Mechanismus zur Wie<strong>der</strong>verwendung zu betrachten. Damit könntenwir ihn auch durch an<strong>der</strong>e Mechanismen, die dies flexibler erreichen (zum BeispielDelegation), ersetzen.• Mehrfaches Subtyping ermöglicht die Konstruktion von flexiblen Systemen, in denengewisse Eigenschaften (Wesenszüge) an beliebigen Stellen in die Klassenhierarchieeingebracht werden können.1 Bei nur zwei Sichtbarkeitsbereichen ist diese Entscheidung unvermeidbar. Der schreibgeschützteExport ist zwar in vielen Fällen hilfreich, aber lei<strong>der</strong> nicht orthogonal zur Sichtbarkeit von Bezeichnern.


7.1 Ergebnisse dieser Arbeit 163• Durch mehrfaches Subtyping können wir Klassifikationen nach mehreren Gesichtspunktendurchführen, ohne mit den Problemen gewöhnlicher Mehrfachvererbung(im Sinne von Subtyping und Subclassing) konfrontiert zu werden. 2DawirindieserArbeitkompatibelmitOberon-2 bleiben wollten, hat schon die Einführungvon Signaturen ohne mehrfaches Subtyping Konsequenzen für das Laufzeitverhalteneines Programms, da die Implementierung des dynamischen Bindens kompliziertereSuchvorgänge bzw. mehrere Indirektionen erfor<strong>der</strong>t. 3 Bei Verzicht auf diese Kompatibilitätkönnten wir die Trennung von (einfachem) Subtyping und Subclassing ohne Verlustvon Effizienz realisieren. Mehrfaches Subtyping führt hingegen immer zu Effizienzverlusten.Die entsprechenden Spracherweiterungen für Signaturen und mehrfaches Subtypigkonnten allerdings in dieser Arbeit nicht vollständig realisiert werden. Der wesentlichGrund dafür ist, daß neben Än<strong>der</strong>ungen am Compiler OP2 auch viele Än<strong>der</strong>ungen imgesamten Oberon-System notwendig geworden wären. Solche weitreichenden Modifikationenwollten wir hier aber — nicht zuletzt aus Zeitgründen — nicht berücksichtigen.Komplexität des CompilersDa die obigen Aussagen zur Komplexität <strong>der</strong> implementierten Spracherweiterungen lediglichqualitativer Natur sind, haben wir in Tabelle 7.1 für drei unterschiedliche Versionenvon OP2 jeweils die Anzahl <strong>der</strong> Anweisungen pro Modul zusammengestellt. Diessollte eine bessere Grundlage für eine quantitative Beurteilung <strong>der</strong> Komplexität bieten.Version/Modul OP2 V1.1 FP1 V0.9 OP2 V1.4OPB.Mod 1386 1400 1408OPC.Mod 1167 1170 1188OPL.Mod 814 819 836OPM.Mod 148 148 148OPP.Mod 1122 1596 1134OPS.Mod 312 322 312OPT.Mod 759 868 768OPV.Mod 540 555 545Tabelle 7.1: Anzahl <strong>der</strong> Anweisungen pro Modul für drei OP2 VersionenIn dieser Tabelle steht OP2 V1.1 für den Compiler, auf den wir in dieser Arbeitaufgebaut haben, FP1 V0.9 ist <strong>der</strong> von uns entwickelte Compiler für <strong>Fro<strong>der</strong>on</strong>-1. ZumVergleich ist noch eine aktuellere Version des Compilers OP2 angegeben, in <strong>der</strong> hauptsächlicheinige Fehler früherer Versionen korrigiert wurden. Wir merken hier noch an,2 Die durch Subtyping und Subclassing entstehenden Graphen müssen nicht isomorph zueinan<strong>der</strong>sein.3 Durch die in Oberon-2 vorhandene Typerweiterung wird auf jeden Fall eine Subtyping-Relationeingeführt. Damit ist für Signaturen zwangsläufig schon doppeltes Subtyping“ vorhanden.”


164 Zusammenfassung und Ausblickdaß wir stets versucht haben, die Erweiterungen so in OP2 einzubringen, daß möglichstwenige Än<strong>der</strong>ungen an bestehenden Prozeduren notwendig waren. Dies führt natürlichdazu, daß in FP1 V0.9 einige Programmteile doppelt vorhanden sind.7.2 Verwandte AnsätzeViele <strong>der</strong> Konzepte, die in dieser Arbeit in Oberon-2 eingebracht wurden, sind — teilsin an<strong>der</strong>en Formen — auch in an<strong>der</strong>en imperativen und objektorientierten Programmiersprachenvorhanden. In diesem Abschnitt gehen wir auf einige dieser Sprachengenauer ein und stellen dabei insbeson<strong>der</strong>e die Unterschiede zu unseren Erweiterungenfür <strong>Fro<strong>der</strong>on</strong>-1 heraus.Formale SpezifikationWie in Kapitel 4 schon erwähnt haben wir unsere Erweiterungen zur formalen Spezifikationhauptsächlich an Eiffel [43, 45] ausgerichtet. Dort sind weitgehend dieselbenKonstrukte vorhanden, die auch von uns als notwendig angesehen werden. Darüber hinausbietet Eiffel aber noch eine Reihe weiterer Konstrukte, die im Rahmen von Oberon-2jedoch unangebracht erscheinen.Im Gegensatz zu <strong>Fro<strong>der</strong>on</strong>-1 und Eiffel unterstützt die auf C++ basierende SpracheA++ [8] auch Prädikatenlogik zur Spezifikation von Klassen. Hier wurde aber eineweitgehende Überprüfung <strong>der</strong> Zusicherungen durch den Compiler selbst angestrebt. Diessteht im Gegensatz zu unserer Prämisse, daß die Prüfung von Zusicherungen zur Laufzeitstattfinden muß, da geeignete Werkzeuge für einen automatischen Beweis zugesicherterAussagen nicht verfügbar sind.Klassen und TypenVollständige Mehrfachvererbung (also Subtyping und Subclassing), von <strong>der</strong> wir bewußtabgesehen haben, wird beispielsweise in den Programmiersprachen Eiffel [43, 45] undC++ [58] unterstützt. Diese Sprachen haben also auch mit den entsprechenden in Kapitel6 geschil<strong>der</strong>ten Problemen zu kämpfen. Sie unterscheiden außerdem nicht zwischenSubtyping und Subclassing.In Portlandish [51] wird sowohl mehrfaches Subtyping als auch mehrfaches Subclassingangeboten, allerdings sind beide Relationen klar voneinan<strong>der</strong> getrennt.Eine aktuelle Programmiersprache, die im wesentlichen unseren asymmetrischen Ansatzvon mehrfachem Subtyping und einfachem Subclassing unterstützt, ist Java [50].Klassen können hier durch das Schlüsselwort extends eine Basisklasse angeben (einfachesSubclassing), während sie durch implements eine Reihe von Signaturen, die inJAVA Interfaces heißen, implementieren können (mehrfaches Subtyping).Ein ähnlicher Ansatz für Oberon-2 wurde auch von Nedorya und an<strong>der</strong>en in [49]vorgestellt, allerdings wurde dieser Vorschlag nicht implementiert. Er ist unserem Vorgehenähnlich, verwendet aber eine an<strong>der</strong>e Terminologie (zum Beispiel ABSTRACT RECORD


7.2 Verwandte Ansätze 165für SIGNATURE) und erlaubt zwischen Signaturen keine Subtyping-Relation. Damit lassensichzwarWeseszügeimplementieren, allgemeinere Klassifikationen sind jedoch ausgeschlossen.In [3] wird eine Erweiterung für Signaturen im Rahmen von C++ diskutiert, dieallerdings wenig Ähnlichkeiten mit unseren Signaturen hat. Zum einen wird dort dieKompatiblität zwischen Signaturen durch strukturelle Äquivalenz definiert, was zu denin Kapitel 6 diskutierten Problemen führt. Zum an<strong>der</strong>en erlaubt C++ with Signaturesdie Deklaration von default implementations, wodurch die dort vorhandenen Signaturennach unserer Terminologie eigentlich eher abstrakte Klassen sind.Sichtbarkeit von KlassenDie Idee des geschützten Sichtbarkeitbereichs stammt im wesentlichen aus C++ [58].Dort wird er aus <strong>der</strong> gleichen Motivation wie in dieser Arbeit angeboten.Die Programmiersprache Java [50] unterscheidet noch feiner zwischen den Sichtbarkeitsbereicheneinzelner Klassen. In JAVA gibt es außerdem ein package-Konstrukt,das teilweise mit Modulen in Oberon-2 vergleichbar ist: Klassen, die in einem packagedeklariert wurden, können ohne jede Beschränkung aufeinan<strong>der</strong> zugreifen. Weiterhinkönnen einzelne Bezeichner dieser Klassen ihre Sichtbarkeit um zwei Stufen nach unten”und oben“ verän<strong>der</strong>n: Das Schlüsselwort protected erlaubt diese Zugriffe auch abgeleitetenKlassen aus einem an<strong>der</strong>en package, public hingegen allen Klassen aus allenpackages. Bezeichner, die als private deklariert wurden, sind hingegen nur in <strong>der</strong> deklarierendenKlasse selbst sichtbar, solche die als private protected deklariert wurdennur in abgeleiteten Klassen, nicht aber in an<strong>der</strong>en Klassen, die dem gleichen packageangehören. Damit verbindet JAVA die Sichtbarkeit in Klassen und Modulen relativorthogonal. Eine Untersuchung, ob und mit welchen Konsequenzen sich ein ähnlichesModell auch für Oberon-2 realisieren läßt, wäre für zukünftige Arbeiten interessant.Seiteneffektfreie FunktionenSeiteneffektfreie Funktionen (genauer: R-Funktionen) werden auch in Oberon-V [26]unterstützt. Diese Sprache baut allerdings auf Oberon und nicht auf Oberon-2 auf.Außerdem werden Funktionen dort nicht durch ein neues Schlüsselwort gekennzeichnet,son<strong>der</strong>n es wird die Semantik von Funktionsprozeduren verän<strong>der</strong>t. Damit ist Oberon-V nicht kompatibel mit Oberon. Später wurden im Rahmen des Oberon-V Compilersauch S-Funktionen unterstützt (siehe [27]), allerdings durch eine dynamische Prüfungzur Laufzeit.


166 Zusammenfassung und Ausblick7.3 Die weitere <strong>Entwicklung</strong> von Oberon und Oberon-2Mit <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> verfolgen wir einen inkrementellen Ansatz, um ausgehend vonOberon-2 durch Experimente mit neuen Sprachkonzepten zu einem möglichen Nachfolgervon Oberon-2 zu gelangen. In diese eher ”konservative“ Vorgehensweise reiht sichauch ein Vorschlag von Roe und Szyperski ein. Sie zeigen in [54], wie Generizität (parametricpolymorphism)inOberon-2 eingebracht werden kann, ohne dabei Kompromissebezüglich <strong>der</strong> Einfachheit und <strong>der</strong> Effizienz <strong>der</strong> Programmiersprache machen zu müssen.Auch die oben schon zitierten Arbeiten [24] und [49] verfolgen eher diesen Ansatz.Daneben wurden in letzter Zeit aber auch eine Reihe von Arbeiten veröffentlicht, dieteilweise stark von Oberon-2 abweichen und deswegen wohl eher als ”Oberon-basiert“bezeichnet werden müßten:• Mit Active Oberon [31] hat die Gruppe um Jürg Gutknecht kürzlich einenNachfolger von Oberon (nicht Oberon-2!) vorgestellt, <strong>der</strong> parallele Prozesse auf <strong>der</strong>Ebene einzelner Objekte unterstützt. Allerdings wurden hier viele <strong>der</strong> Grundsätzevon Oberon und Oberon-2 aufgegeben o<strong>der</strong> zumindest ”neu interpretiert“. So wurdenRecordtypen in dem Sinne zu Klassen erweitert, daß Methoden direkt als Teildes Recordtyps deklariert werden können. Ein Konzept, das auch für <strong>Fro<strong>der</strong>on</strong>-1und <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> interessant werden könnte, ist die Einführung von Konstruktoren(initializers). Durch eine Umsetzung ihrer Definition in Oberon-2 könntenwir Invarianten für Klassen in <strong>Fro<strong>der</strong>on</strong>-1 integrieren.• Bei Action-Oberon [2] wurde ebenfalls Parallelverarbeitung in den Vor<strong>der</strong>grundgestellt. Diese Sprache baut auf Oberon-2 auf, und die Semantik <strong>der</strong> Parallelitätist in ihr wohldefiniert, da sie auf einem exakten formalen Modell (theory of actionsystems) beruht.• Mit Lagoona [18] wurde von Michael Franz ein Nachfolger von Oberon vorgestellt,<strong>der</strong> sich hauptsächlich durch die Einführung von Kategorien, die unserenSignaturen entsprechen, von Oberon und Oberon-2 unterscheidet. Beson<strong>der</strong>s interessantist die Tatsache, daß die Signaturen und <strong>der</strong>en Methoden nicht in einemsyntaktischen Konstrukt vereint sind. Aufrufe von Methoden (messages) sind eineigenständiges Konzept. Anstelle von Vererbung im herkömmlichen Sinn wird außerdemein Delegationsmechanismus verwendet, <strong>der</strong> insbeson<strong>der</strong>e Vorteile bei <strong>der</strong><strong>Entwicklung</strong> eigenständiger und erweiterbarer ”black-box“-Komponenten hat.Diese teilweise sehr verschiedenen Ansätze machen deutlich, daß eine Aussage über dieweitere <strong>Entwicklung</strong> <strong>der</strong> Programmiersprachen Oberon und Oberon-2 sehr schwierig ist.Die Vielzahl an Vorschlägen für mögliche Nachfolger ist für Anwen<strong>der</strong> jedoch sehrverwirrend. Zu einem großen Teil geht man dort von Oberon-2 als Standard aus undist nur bereit, die eine o<strong>der</strong> an<strong>der</strong>e Än<strong>der</strong>ung zu akzeptieren, wenn weitgehende Kompatibilitätzu bestehenden Programmen besteht. Auch <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> wird es schwerhaben, einen Nachfolger für Oberon-2 so zu definieren, daß er auf breiten Zuspruch <strong>der</strong>Anwen<strong>der</strong> trifft.


7.3 Die weitere <strong>Entwicklung</strong> von Oberon und Oberon-2 167Allgemein können wir für die <strong>Entwicklung</strong> zukünftiger Programmiersprachen jedochfesthalten, daß Oberon und Oberon-2 durch ihren kleinen Umfang geeignete Ausgangspunktefür viele Experimente bieten. Solche Experimente sollten sich unserer Meinungnach auch mit den folgenden Fragen beschäftigen, die während <strong>der</strong> Arbeit an <strong>Fro<strong>der</strong>on</strong>-1aufgetreten sind:• Durch formale Wertparameter werden implizit Kopien <strong>der</strong> übergebenen aktuellenParameter angelegt. Ist das wünschenswert? Sollten diese Kopien nicht besserexplizit durch den Programmierer angelegt werden? Welche Konsequenzen hättedie Entscheidung, Wertparameter nicht zu unterstützen? Können wir eine Programmiersprache,die nur Referenzparameter unterstützt, sicher genug machen?• Können durch eine geeignete Kombination von Signaturen und formaler Spezifikationechte abstrakte Datentypen als Beschreibung von Schnittstellen eingesetztwerden? Können wir diese so gestalten, daß sie von Modulen o<strong>der</strong> Klassen implementiertwerden können, ohne daß ein Klient anhand <strong>der</strong> Schnittstelle erkennt,welche Implementierung gewählt wurde?• Seiteneffekte sind durch Zeiger, Referenzparameter und Blockstruktur möglich.Ist Blockstruktur aber wirklich <strong>der</strong> Weisheit letzter Schluß? Lassen sich auch ineiner Sprache ohne Blockstruktur, die keine Zugriffe auf umgebende Sichtbarkeitsbereichezuläßt, brauchbare Systeme entwickeln? Könnte Blockstruktur expliziteingeführt werden, etwa so, daß Prozeduren den Zugriff auf umgebende Variablenausdrücklich kennzeichnen müssen (vergleichbar dem Import von Modulen)?Die Vielzahl an Möglichkeiten und Unsicherheiten, die sich im Bereich <strong>der</strong> Programmiersprachenauftut, sollte uns nicht davor abschrecken, weiterhin an neuen und innovativenIdeen zu arbeiten und dabei vielleicht auch Fehler zu machen, aus denen wir selbst o<strong>der</strong>an<strong>der</strong>e lernen können. Ich persönlich werde mich — sobald <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong> einen befriedigendenAbschluß gefunden hat o<strong>der</strong> in ein echtes net.project übergegangen ist —einer neuen Programmiersprache mit dem Arbeitstitel ARIEL (Akronym für Another ”Really Interesting Experimental Language“) widmen. Aber das ist eine an<strong>der</strong>e Geschichte...


Anhang AWeitere Verbesserungsvorschläge fürOberon-2Einige weitere Probleme <strong>der</strong> Sprachdefinition von Oberon-2 wurden schon von an<strong>der</strong>enidentifiziert und werden hier kurz zusammengefasst um eine möglichst umfassendeAuflistung zu bieten. All diese Probleme sollen im Rahmen von <strong>Projekt</strong> <strong>Fro<strong>der</strong>on</strong>berücksichtigt werden, wie auch eine ganze Reihe von Konzepten aus neueren Programmiersprachen,die auf Oberon-2 basieren.


170 Weitere Verbesserungsvorschläge für Oberon-2A.1 Unregelmäßigkeiten <strong>der</strong> SyntaxIn <strong>der</strong> konkreten Syntax von Oberon-2 sind einige Unregelmäßigkeiten vorhanden, dieallerdings vornehmlich kosmetischer Natur sind. So werden zum Beispiel das logischeUND sowie das logische NICHT durch die Symbole & und ~ repräsentiert, während fürdas logische ODER das Schlüsselwort OR verwendet wird, obwohl das Symbol | in diesemZusammenhang sinnvoller erscheint.Durch einige einfache Modifikationen <strong>der</strong> Syntax, die zum Beispiel in [26] beschriebensind, können viele dieser Probleme behoben werden.A.2 Unstrukturierte AnweisungenIn Oberon-2 existieren zwei (bzw. drei) Anweisungen, die sich nicht mit den Idealen <strong>der</strong>strukturierten Programmierung in Einklang bringen lassen.Als erstes ist hier die RETURN-Anweisung zu nennen, die an beliebigen Stellen in einerProzedur verwendet werden kann und jeweils einem GOTO an das Ende dieser Prozedurentspricht. In einem eng begrenzten Rahmen (z.B. bei <strong>der</strong> Überprüfung einiger Vorbedingungenam Anfang einer Prozedur) kann dies sinnvoll sein, allerdings besteht dieGefahr, das Prozeduren, die diese Möglichkeit ausgiebig nutzen, nur noch schwer lesbarsind.Als zweites (bzw. drittes) sind die LOOP- und EXIT-Anweisungen zu nennen, diezusammen die Konstruktion allgemeiner Schleifen ermöglichen, sich aber nicht vernünftig(zum Beispiel durch axiomatische Semantik, siehe Kapitel 4) theoretisch beschreibenlassen.Sowohl in [26] als auch in [5] wird belegt, daß diese Konstrukte sich fast ausschließlichstörend auf die <strong>Entwicklung</strong> mo<strong>der</strong>ner Compiler für Oberon-2 auswirken und vonvielen Programmierern ohnehin selten genutzt werden. In [5] wird auch ein Verfahrenvorgestellt, daß diese unstrukturierten Anweisungen auf strukturierte Anweisungen abbildenkann, allerdings stellt sich die Frage, ob dieser Aufwand wirklich sinnvoll ist, umdie Abwärtskompatibilität nicht zu gefährden.Eine einfachere Lösung dieser Probleme wäre das Entfernen <strong>der</strong> LOOP- und EXIT-Anweisungen aus den Sprache, sowie das Ersetzen <strong>der</strong> RETURN-Anweisung durch einemodifizierte Syntax für Prozedurdeklarationen, etwa durchProcDecl = "PROCEDURE" [Receiver] IdentDef [FormalPars] ";"DeclSeq ["BEGIN" StatementSeq ["RETURN" Expr]] "END" ident.o<strong>der</strong> eine ähnliche Formulierung, die eine syntaktische Bindung an die Prozedur erlaubt.A.3 Prozedurtypen- und variablenObwohl in Oberon-2 Typgleichheit weitgeheng durch Names-Äquivalenz geprüft wird,besteht für Prozeduren eine Ausnahme: Da <strong>der</strong> Typ einer Prozedur durch ihre formalenParameter und ihren Ergebnistyp bestimmt wird, Prozeduren aber nicht durch


A.4 Vergleich von Prozedurvariablen 171eine explizite Typvereinbarung deklariert werden, hätten zwei Prozeduren mit gleichenformalen Parametern und Ergebnistypen bei Namens-Äquivalenz immer verschiedeneTypen (ähnlich zwei seperat aber vom Aufbau her identisch deklarierten Records o<strong>der</strong>Arrays). Aus diesem Grund verwendet Oberon-2 bei <strong>der</strong> Zuweisung einer Prozedur aneine Prozedurvariable strukturelle Äquivalenz zur Typprüfung.Die Kompatibilitätsregeln zwischen Prozedurtypen- und variablen sowie deklariertenProzeduren sind in Oberon-2 für einige Überraschungen gut, wie das folgende aus [26]entnommene Beispiel zeigt:TYPET = PROCEDURE (x, y: INTEGER; VAR z: REAL);VARv: T;PROCEDURE P (x, y: INTEGER; VAR z: REAL);PROCEDURE P1 (p: PROCEDURE (x, y: INTEGER; VAR z: REAL));PROCEDURE P2 (p: T);Wegen <strong>der</strong> strukturellen Äquivalenz ist es möglich die Prozedur P als Parameter an dieProzeduren P1 und P2 zu übergeben, o<strong>der</strong> sie <strong>der</strong> Variablen v zuzuweisen.Nun kann auf <strong>der</strong> einen Seite aber innerhalb von P1 <strong>der</strong> Parameter p nicht andie Variable v zugewiesen werden, weil bei Zuweisungen zwischen Variablen Namens-Äquivalenz verwendet wird. Allerdings ist es möglich P1 rekursiv mit dem Parameter p(nicht aber mit v) aufzurufen, da hier <strong>der</strong> Typ des aktuellen und des formalen Parameterszufällig“ übereinstimmt.”Auf <strong>der</strong> an<strong>der</strong>en Seite kann innerhalb von P2 <strong>der</strong> Parameter p an die Variable vzugewiesen werden und P2 kann sich rekursiv mit p (o<strong>der</strong> v) als aktuellen Parameteraufrufen. Allerdings ist es nicht möglich P1 mit p als aktuellem Parameter aufzurufen.Die Lösung dieses Problems ist die komplette Umstellung von allen ProzedurbezogenenSprachelementen auf strukturelle Äquivalenz. In [26] wurde dies Beispielsweise fürOberon-V durchgeführt.A.4 Vergleich von ProzedurvariablenDer Vergleich zwischen Prozedurvariablen und deklarierten Prozeduren (sozusagen Prozedurkonstanten)ist in Oberon-2 nicht erlaubt, wohl aber <strong>der</strong> Vergleich zweier Prozedurvariablen.Wenn ein solcher Vergleich nötig und sinnvoll ist (wie zum Beispiel bei Anwendungen<strong>der</strong> Metaprogrammierung 1 )mußalsozunächst wie in [61] beschrieben eine temporäreProzedurvariable für den Vergleich eingeführt werden.1 Aus [61]: Der Begriff Metaprogrammierung bezieht sich auf Programmieren auf <strong>der</strong> Ebene <strong>der</strong>Interpretation eines Programmes, o<strong>der</strong> in an<strong>der</strong>en Worten, auf das Erweitern des Interpreters einergegebenen Programmiersprache in einem anwendungsbezogenen Sinn.


172 Weitere Verbesserungsvorschläge für Oberon-2Da die Realisierung eines solchen Vergleichs zwischen Prozedurvariable und Prozedurkonstantelediglich auch einen Vergleich <strong>der</strong> referenzierten bzw. realen Adressehinausläuft, scheint diese Einschränkung überflüssig. 2A.5 Zeigertypen und <strong>der</strong>en DeklarationWie in Abschnitt 2.4.1 beschrieben induziert die Typerweiterung zwischen Records aucheine Typerweiterung zwischen Zeigertypen, die an diese Records gebunden sind. Diesführt im Zusammenhang mit Arrays auf eine weitere Überraschung, die im Interesse<strong>der</strong> Minimierung von Überraschungseffekten vermieden werden sollte. Wir betrachtenfolgendes Beispiel aus [26]:TYPEP = POINTER TO R;Q = POINTER TO R;R = RECORD (* ... *) END;VARp: P; q: Q;(* ... *)BEGINp := q; q := p;Da R die ”nullte“ Erweiterung von sich selbst ist, sind beide Zuweisungen zwischenp und q erlaubt, obwohl P und Q verschiedene Typen sind. Wenn wir allerdings R =ARRAY 32 OF INTEGER vereinbaren würden, wären die Zuweisungen nicht zulässig, dazwischen Arrays ja keine <strong>der</strong> Typerweiterung von Records entsprechende Beziehung besteht.Um dieses Problem zu beheben werden in [26] zwei Zeigertypen dann als gleichdefiniert, wenn sie den gleichen Basistyp haben. Dies vermeidet die oben aufgezeigteUnregelmäßigkeit und macht außerdem die Fortsetzung <strong>der</strong> Typerweiterung auf Zeigertypenüberflüssig.Ein zweites Problem ist die Deklaration von Klassen in Oberon-2, dieüblicherweisesowohl den Zeigertypen als auch den Recordtypen beinhaltet. Hier wird oft eine <strong>der</strong>beiden Konventioneno<strong>der</strong>TYPEClass = POINTER TO ClassDesc;ClassDesc = RECORD (* ... *) ENDTYPEClassPtr = POINTER TO Class;Class = RECORD (* ... *) END2 Probleme mit seperaten Adressräumen für mehrere Prozessoren wurden in Oberon-2 ja ohnehinaußer acht gelassen.


A.5 Zeigertypen und <strong>der</strong>en Deklaration 173benutzt, um zu kennzeichnen, das hier eine Klassendeklaration vorliegt. Häufig wird im<strong>weiteren</strong> aber nur einer <strong>der</strong> Typen—meist <strong>der</strong> Zeigertyp—verwendet. Anstatt sich ansolche Konventionen zu halten ist es besser, ihre Notwendigkeit komplett zu vermeiden.Deswegen wird in [26] vorgeschlagen die Deklaration von Zeigertypen dort zu machen, wosie wirklich gebraucht werden, und die oben beschriebenen Konventionen zu verlassen:TYPEClass = RECORD (* ... *) END;(* ... *)PROCEDURE (self: ^Class) Method ();(* ... *)END Method;In diesem Beispiel wird für den Empfängerparameter von Method() ein Zeiger <strong>der</strong> FormPOINTER TO Class deklariert. Das Symbol ^ ist lediglich eine kürze Schreibweise. Zusammenmit den oben erläuterten Kompatibilitätsregeln für Zeigertypen vereinfacht dieseForm <strong>der</strong> Deklaration Oberon-2 weiter ohne gravierende Nachteile zu haben.


Literaturverzeichnis[1] S. Alagić und M. Arbib. The Design of Well-structured and CorrectPrograms. Texts and Monographs in Computer Science. Springer-Verlag, 1978.ISBN 3-540-90299-6.[2] R.Back,M.Büchi und E. Sekerinski. Adding Type-Bound Actions toAction-Oberon. Technischer Bericht 66, Department of Computer Science, AboAkademi University, Turku Centre for Computer Science, November 1996.[3] G. Baumgartner. Modularization Constructs for Functional andObject-oriented Languages. Doktorarbeit, Purdue University, August 1996.[4] G. Booch. Object-Oriented Analysis and Design with Applications. SeriesinObject-Oriented Software Engineering. Addison-Wesley Publishing Company,Menlo Park, 2. Auflage, 1994. ISBN 0-8053-5340-2.[5] M. M. Brandis. Optimizing Compilers for Structured Programming Languages.Doktorarbeit, Eidgenössische Technische Hochschule, Zürich, 1995.[6] M. M. Brandis, R. Crelier, M. Franz und J. Templ. The Oberon SystemFamily. Technischer Bericht 174, Eidgenössische Technische Hochschule, Zürich,April 1992.[7] R. Budde und K.-H. Sylla. From Application Domain Modelling to TargetSystems. In: R. Budde, K. Kuhlenkampf, L. Mathiassen und H. Züllighoven(Redakteure), Approaches to Prototyping, Seiten 31–48, Berlin, 1984.Springer-Verlag. ISBN 3-540-13490-5.[8] M. Cline und D. Lea. The Behavior of C++ Classes. In: Proceedings of theSymposium on Object Oriented Programming Emphasizing Practical Applications,Marist College, 1990.[9] D. Corney und J. Gough. Type Test Elminination using Typeflow Analysis.In: Proceedings of Programming Languages and System Architectures, Nummer782 in Lecture Notes in Computer Science. Springer Verlag, März 1994. Verfügbarbei [23].[10] R. Crelier. OP2: A Portable Oberon-2 Compiler. In: Proceedings of the 2ndInternational Modula-2 Conference, Seiten 58–67, Loughborough, England, 1991.


176 Literaturverzeichnis[11] R. Crelier. Separate Compilation and Module Extension. Doktorarbeit,Eidgenössische Technische Hochschule, Zürich, 1994.[12] A. Disteli. Oberon for PC on an MS-DOS Base. Technischer Bericht 203,Eidgenössische Technische Hochschule, Zürich, November 1993.[13] ETH Zürich. ETHZürich Departement Informatik Homepage. URL:http://www.inf.ethz.ch. Zentrale Homepage des Departements Informatik <strong>der</strong>ETH Zürich.[14] M. Franz. Juice Homepage. URL: http://www.ics.uci.edu/˜juice. Homepage desJuice-<strong>Projekt</strong>s.[15] M. Franz. The Implementation of MacOberon. Technischer Bericht 141,Eidgenössische Technische Hochschule, Zürich, Oktober 1990.[16] M. Franz. Code-Generation On-the-Fly: A Key to Portable Software.Doktorarbeit, Eidgenössische Technische Hochschule, Zürich, 1994.[17] M. Franz. Protocol Extension: A Technique for Structuring Large ExtensibleSoftware-Systems. Technischer Bericht 226, Eidgenössische TechnischeHochschule, Zürich, Dezember 1994.[18] M. Franz. The Programming Language Lagoona: A Fresh Look atObject-Orientation. Technischer bericht, University of California, Department ofInformation and Computer Science, Irvinve, CA92697-3425, 1996. Verfügbarbei [40].[19] A. Frick, R. Neumann und W. Zimmermann. Eine Methode zurKonstruktion robuster Klassenhierarchien. Softwaretechnik-Trends, 16 (3): 16–23,September 1996. Son<strong>der</strong>heft mit Beiträgen <strong>der</strong> GI-Fachtagung Softwaretechnik 96.[20] A. Frick, W. Zimmer und W. Zimmermann. Über die Konstruktion robusterobjektorientierter Klassenbibliotheken. Softwaretechnik-Trends, 15 (3): 35–46,Oktober 1995. Son<strong>der</strong>heft mit Beiträgen <strong>der</strong> GI-Fachtagung Softwaretechnik 95.[21] E. Gamma, R. Helm, R. Johnson und J. Vlissides. Design Patterns.Professional Computing Series. Addison-Wesley Publishing Company, 2. Auflage,1994. ISBN 0-201-63361-2.[22] M. Gitsels. MPE: Designing a Proof-Editor for Predicate Calculus. Technischerbericht, Eidgenössische Technische Hochschule, Zürich, 1996.[23] J. Gough. Homepage. URL: http://www.dstc.qut.edu.au/˜gough. Homepagevon John Gough, dem Autor <strong>der</strong> Gardens Point Compiler. Hier finden sich auchviele Publikationen mit Bezug zu Oberon und Oberon-2.


Literaturverzeichnis 177[24] J. Gough und H. Klaeren. Executable Assertions and Separate Compilation.In: Proceedings of the Joint Modular Languages Conference Linz, Austria, 1997.Verfügbar bei [23].[25] T. Grams. Denkfallen und Programmierfehler. Springer Compass. SpringerVerlag, Berlin, Heidelberg, 1990. ISBN 3-540-52039-2.[26] R. Griesemer. A Programming Language for Vector Computers. Doktorarbeit,Eidgenössische Technische Hochschule, Zürich, 1993.[27] R. Griesemer. Detection of Side-Effects in Function Procedures. TechnischerBericht TR-94-032, International Computer Science Institute, Berkeley, August1994. URL: ftp://ftp.icsi.Berkeley.edu/pub/techreports/1994/tr-94-032.ps.Z.[28] R. Griesemer, C. Pfister, B. Heeb und J. Templ. On the Linearization ofGraphs and Writing Symbol Files / Oberon Technical Notes. Technischer Bericht156, Eidgenössische Technische Hochschule, Zürich, März 1991.[29] R. H. Güting. Datenstrukturen und Algorithmen. Leitfäden und Monographien<strong>der</strong> Informatik. B. G. Teubner Verlag, Stuttgart, 1992. ISBN 3-519-02121-8.[30] J. Gutknecht. Oberon, Gadgets and Some Archetypal Aspects of PersistentObjects. Technischer Bericht 243, Eidgenössische Technische Hochschule, Zürich,Februar 1996.[31] J. Gutknecht. Do the Fish Really Need Remote Control? A Proposal forSelf-Active Objects in Oberon. In: Proceedings of the Joint Modular LanguagesConference Linz, Austria, 1997. To be published. URL:ftp://ftp.inf.ethz.ch/pub/Oberon/System3/Native/System/aopaper.ps.[32] G. Gygax. A Guide to the World of Greyhawk Fantasy Setting. In: World ofGreyhawk Fantasy Game Setting. TSR, 1983.[33] D. Hall und A. Rood. Microprocessors and Interfacing — Programming andHardware — 68000 Version. Glencoe Division of Macmillan / McGraw-Hill SchoolPublishing, Westerville, 1993. ISBN 0-07-025691-8.[34] K. Hildebrand und J.-A. Reepmeyer. Repeat-Statement consi<strong>der</strong>edharmful? Ergebnisse einer empirischen Untersuchung. Informatik-Spektrum,19 (2): 68–70, April 1996.[35] W. Hilf und A. Nausch. Grundlagen und Architektur, Band1vonM68000Familie. te-wi Verlag, München, 1984. ISBN 3-921803-16-0.[36] J. E. Hopcroft und J. D. Ullman. Introduction to Automata Theory,Languages and Computation. Series in Computer Science. Addison-WesleyPublishing Company, Reading, Massachusetts, 1979. ISBN 0-201-02988-X.


178 Literaturverzeichnis[37] W. Kamlah und P. Lorenzen. Logische Propädeutik — Vorschule desvernünftigen Redens. B.I. Wissenschaftsverlag, Mannheim, 2. Auflage, 1973. ISBN3-411-05227-9.[38] B. Kirk. The Oakwood Guidelines for Oberon-2 Compiler Developers.PostScript document, 1993. Kann von Brian Kirk angefor<strong>der</strong>t werden.[39] D. E. Knuth. Structured Programming with goto Statements, Seiten 17–89.Nummer 27 in CSLI Lecture Notes. Center for the Study of Language andInformation, Leland Stanford Junior University, 1992. ISBN 0-9370-7380-6.[40] G. Laden. The Oberon Reference Site. URL:http://www.math.tau.ac.il/˜laden/Oberon. Inoffizielle Homepage zu allem undjedem rund um Oberon.[41] S. Lalis und B. San<strong>der</strong>s. Adding Concurrency to the Oberon System. In:Proceedings of Programming Languages and System Architectures, Nummer 782 inLecture Notes in Computer Science. Springer Verlag, März 1994. URL:ftp://ftp.inf.ethz.ch/pub/software/Oberon/Docu/concurrentOberon.ps.gz.[42] O. L. Madsen, B. Møller-Pe<strong>der</strong>sen und K. Nygaard. Object-OrientedProgramming in the Beta Programming Language. ACM Press. Addison-WesleyPublishing Company, Wokingham, 1993. ISBN 0-201-62430-3.[43] B. Meyer. Object-oriented Software Construction. Series in Computer Science.Prentice-Hall International, 1988. ISBN 0-13-629031-0.[44] B. Meyer. Introduction to the Theory of Programming Languages. SeriesinComputer Science. Prentice-Hall International, 1990. ISBN 0-13-498502-8.Korrigierter Nachdruck 1991.[45] B. Meyer. Eiffel: The Language. Object-Oriented Series. Prentice-HallInternational, 1992. ISBN 0-13-247925-7.[46] H. Mössenböck. Differences Between Oberon and Oberon-2 / TheProgramming Language Oberon-2. Technischer Bericht 160, EidgenössischeTechnische Hochschule, Zürich, Oktober 1993.[47] H. Mössenböck. Objektorientierte Programmierung in Oberon-2.Springer-Verlag, Berlin, 1993. ISBN 3-540-55690-7.[48] Motorola. MC68000 16-Bit Microprocessor User’s Manual. Prentice-Hall, Inc.,Englewood Cliffs, 3. Auflage, 1982. ISBN 0-13-566695-3.[49] A. E. Nedorya, E. V. Tarasov und A. D. Hapugin. Restricted MultipleInheritance. In: Proceedings of the Joint Modular Languages Conference, Seiten21–29, Ulm, Germany, 1994.


Literaturverzeichnis 179[50] P. Niemeyer und J. Peck. Exploring JAVA. O’Reilly & Associates, 1996.ISBN 1-56592-184-4.[51] H. Porter. Separating the Subtype Hierarchy from the Inheritance ofImplementation. Journal of Object-Oriented Programming, 4 (9): 20–29, February1992.[52] K. Quibeldey-Cirkel. Das Objekt-Paradigma in <strong>der</strong> Informatik. B.G. TeubnerStuttgart, 1994. ISBN 3-519-02295-8.[53] M. Reiser und N. Wirth. Programming in Oberon: Steps beyond Pascal andModula. ACM Press. Addison-Wesley Publishing Company, Wokingham, 1992.ISBN 0-201-56543-9.[54] P. Roe und C. Szyperski. Lightweight Parametric Polymorphism for Oberon.Technischer bericht, Queensland University of Technology, Brisbane QLD 4001,Australia, 1997. Verfügbar bei [40].[55] U. Schöning. Theoretische Informatik kurz gefasst. BI-Wissenschafts-Verlag,Mannheim, 1992. ISBN 3-411-15641-4.[56] R. Sebesta. Concepts of Programming Languages. Addison-Wesley PublishingCompany, 3. Auflage, 1996. ISBN 0-8053-7133-8.[57] F. Siebert. Amiga Oberon Compiler 3.0. A+LAG,Dä<strong>der</strong>iz 61, CH-2540Grenchen, 1990.[58] B. Stroustrup. The C++ Programming Language. Addison-Wesley PublishingCompany, Reading, 2. Auflage, 1991. ISBN 0-201-53992-6. KorrigierterNachdruck, Februar 1993.[59] C. Szyperski. InsightETHOS:OnObject-OrientationinOperatingSystems.Nummer 40 in Informatik-Dissertationen ETH Zürich. Verein <strong>der</strong> Fachverlage,Zürich, 1992. ISBN 3-7281-1948-2.[60] J. Templ. SPARC-Oberon User Guide / SPARC-Oberon Implementation.Technischer Bericht 133, Eidgenössische Technische Hochschule, Zürich,September 1991.[61] J. Templ. Metaprogramming in Oberon. Doktorarbeit, Eidgenössische TechnischeHochschule, Zürich, 1994.[62] A. N. Whitehead und B. Russell. Principia Mathematica—Vorwort undEinleitungen, Band 593 von Suhrkamp-Taschenbuch Wissenschaft. SuhrkampVerlag, Frankfurt am Main, 2. Auflage, 1990. ISBN 3-518-28193-3.[63] N. Wirth. From Modula to Oberon / The Programming Language Oberon(revised). Technischer Bericht 143, Eidgenössische Technische Hochschule, Zürich,November 1990.


180 Literaturverzeichnis[64] N. Wirth. Grundlagen und Techniken des Compilerbaus. Addison-WesleyVerlag, 1996. ISBN 3-89319-931-4.[65] N. Wirth und J. Gutknecht. Project Oberon: The Design of an OperatingSystem and Compiler. ACM Press. Addison-Wesley Publishing Company,Wokingham, 1992. ISBN 0-201-54428-8.

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!