Kapitel 11 - Grundlagen der Programmiersprachen - DdI
Kapitel 11 - Grundlagen der Programmiersprachen - DdI
Kapitel 11 - Grundlagen der Programmiersprachen - DdI
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
<strong>11</strong>-1<br />
<strong>11</strong> <strong>Grundlagen</strong> <strong>der</strong> <strong>Programmiersprachen</strong><br />
In diesem Abschnitt verfolgen wir das Ziel, <strong>Programmiersprachen</strong> exakt zu beschreiben.<br />
Die beiden wichtigsten Aspekte, die eine Programmiersprache kennzeichnen, sind Syntax<br />
und Semantik.<br />
Die Syntax legt fest, welche Aneinan<strong>der</strong>reihungen von Zeichen korrekte Sätze <strong>der</strong> Sprache<br />
sind. Die Semantik bestimmt die inhaltliche Bedeutung aller Sätze <strong>der</strong> Sprache.<br />
Syntax und Semantik bilden also ein untrennbares Paar, wenn es um die Beschreibung<br />
von Sprachen, und speziell von <strong>Programmiersprachen</strong> geht. Folglich definiert man eine<br />
Programmiersprache P als ein Paar P=(L,F), wobei L⊆A * eine formale Sprache über<br />
dem Alphabet A={a,...,z,A,...Z,0,...,9,:,;,-,...} ist und die Syntax von P bestimmt und F eine<br />
Abbildung, die jedem Wort (Programm) w∈L seine Bedeutung (Semantik) zuordnet.<br />
Für die exakte Beschreibung von Syntax und Semantik von <strong>Programmiersprachen</strong> gibt<br />
es eine Reihe von Methoden, von denen wir die wichtigsten im folgenden vorstellen.<br />
<strong>11</strong>.1 Syntax<br />
In <strong>Kapitel</strong> 2 hatten wir bereits gesehen, daß <strong>Programmiersprachen</strong> stets eine feste, präzise<br />
Syntax besitzen müssen. In dieser Kurseinheit definieren wir zunächst den Begriff<br />
<strong>der</strong> Sprache und führen anschließend einige Methoden ein, mit denen man die Syntax<br />
von Sprachen exakt festlegen kann.<br />
<strong>11</strong>.1.1 Alphabet und Sprache<br />
(Natürliche) Sprachen (Deutsch, Englisch, Französisch, Japanisch, Kisuaheli usw.) werden<br />
von Menschen zum Informationsaustausch und zu allen Zwecken <strong>der</strong> Kommunikation<br />
verwendet. Die Beherrschung einer Sprache und ihrer Ausdrucksmöglichkeiten<br />
beeinflußt stark die Vorstellungswelt und die Denkweise von Menschen. Zu je<strong>der</strong> natürlichen<br />
Sprache existiert in <strong>der</strong> Regel eine Schriftsprache; manche Sprachen (z.B. Hethitisch)<br />
liegen nur noch in dieser Form vor.<br />
In <strong>der</strong> Informatik interessiert man sich für künstliche Sprachen, unter denen die <strong>Programmiersprachen</strong><br />
die wichtigsten sind. Künstliche Sprachen wurden Ende des 19. Jahrhun<strong>der</strong>ts<br />
entwickelt, um Fakten, Denkabläufe, Schlußfolgerungen beschreiben und analysieren<br />
zu können. Neben diesen meist in <strong>der</strong> Mathematik verwendeten logischen<br />
Kalkülen entstanden mit <strong>der</strong> Entwicklung von Computern seit 1940 <strong>Programmiersprachen</strong>.
<strong>11</strong>-2<br />
Schriftsprachen (im Gegensatz zu Bildsprachen, z.B. Chinesisch), zu denen auch die<br />
<strong>Programmiersprachen</strong> gehören, sind über einem Alphabet, d.h. einer (endlichen) Menge<br />
von unterscheidbaren Zeichen (Buchstaben) aufgebaut. Die Buchstaben sind geordnet,<br />
d.h., es liegt fest, welches <strong>der</strong> erste, <strong>der</strong> zweite, <strong>der</strong> dritte usw. Buchstabe des Alphabets<br />
ist.<br />
Durch Aneinan<strong>der</strong>reihung endlich vieler Buchstaben eines Alphabets kann man Wörter<br />
(Zeichenfolgen) bilden. Eine Sprache über einem Alphabet ist dann eine Teilmenge <strong>der</strong><br />
Menge aller Wörter, die man mit den Buchstaben des Alphabets bilden kann.<br />
Diese sehr unpräzise Erläuterung <strong>der</strong> Begriffe wollen wir nun formalisieren.<br />
Definition A:<br />
Eine (zweistellige) Relation R auf einer Menge M ist eine Teilmenge R⊆M×M. Sind<br />
a,b∈M und (a,b)∈R, so sagt man "a und b stehen in <strong>der</strong> Relation R". Gelegentlich<br />
schreibt man statt (a,b)∈R auch aRb. Eine Relation heißt<br />
- reflexiv, wenn für alle a∈M stets (a,a)∈R gilt;<br />
- transitiv, wenn für alle a,b,c∈M aus (a,b)∈R und (b,c)∈R stets (a,c)∈R folgt;<br />
- symmetrisch, wenn für alle a,b∈M aus (a,b)∈R auch (b,a)∈R folgt;<br />
- antisymmetrisch, wenn für alle a,b∈M aus (a,b)∈R und (b,a)∈R stets a=b folgt.<br />
Beispiele:<br />
1) Man betrachte eine Verwandschaftsrelation. M sei die Menge aller Menschen.<br />
R⊆M×M sei die Menge aller Paare von Menschen, die miteinan<strong>der</strong> verwandt sind,<br />
d.h. (a,b)∈R, wenn die Personen a und b miteinan<strong>der</strong> verwandt sind.<br />
R ist nicht reflexiv, denn eine Person ist nicht mit sich selbst verwandt (Man kann das<br />
aber auch an<strong>der</strong>s sehen). R ist symmetrisch, denn wenn von zwei Menschen a,b die<br />
Person a mit b verwandt ist ((a,b)∈R), dann ist auch b mit a verwandt ((b,a)∈R). R ist<br />
nicht antisymmetrisch.<br />
Ist R transitiv Wenn von drei Personen a,b,c die Person a mit b und b mit c verwandt<br />
ist ((a,b)∈R und (b,c)∈R), dann ist meist auch a mit c verwandt ((a,c)∈R). Lei<strong>der</strong> gilt<br />
dies aber nicht immer; denn wenn z.B. Ehepartner jeweils ein Kind a und c, die nicht<br />
verwandt sind ((a,c)∉R), in eine Ehe einbringen und dann ein gemeinsames Kind b<br />
haben, dann ist b sowohl mit a als auch mit c verwandt (Halbgeschwister), also folgt<br />
aus (a,b)∈R und (b,c)∈R im allgemeinen nicht (a,c)∈R.<br />
2) Man betrachte die Kleiner-Gleich-Relation. M sei gleich <strong>der</strong> Menge <strong>der</strong> natürlichen<br />
Zahlen IN. R⊆IN×IN sei die Menge aller Paare natürlicher Zahlen (a,b), für die a≤b<br />
gilt. R ist reflexiv, transitiv und antisymmetrisch.
<strong>11</strong>-3<br />
Definition B:<br />
a) Eine reflexive, transitive und symmetrische Relation R⊆M×M auf einer Menge M heißt<br />
Äquivalenzrelation.<br />
b) Eine reflexive, transitive und antisymmetrische Relation heißt (partielle) Ordnung<br />
o<strong>der</strong> Halbordnung.<br />
c) Ist die Ordnung R total definiert, d.h., gilt für alle a,b∈M die Beziehung (a,b)∈R o<strong>der</strong><br />
(b,a)∈R, so ist R eine lineare o<strong>der</strong> totale Ordnung.<br />
Bezeichnung: Ist R eine Ordnung, so schreibt man anstelle von (a,b)∈R meist a≤b. Ist R<br />
antisymmetrisch und transitiv, so schreibt man meist a
<strong>11</strong>-4<br />
Die Länge von w bezeichnet man durch |w|=k. Das Wort <strong>der</strong> Länge 0 heißt leeres<br />
Wort und wird mit ε bezeichnet. Seien v=b 1 b 2 ...b i und w=c 1 c 2 ...c j zwei Wörter über A. vw<br />
ist das Wort, das sich durch Konkatenation (Aneinan<strong>der</strong>fügen) aus v und w ergibt, d.h.<br />
vw=b 1 b 2 ...b i c 1 c 2 ...c j .<br />
Diese Operation ist assoziativ und besitzt mit dem leeren Wort ein Einselement. Die<br />
Menge aller Wörter über A, das sogenannte freie Monoid über A, wird mit A * bezeichnet<br />
und ist definiert durch:<br />
A * ={b 1 b 2 ...b k | b i ∈A für i=1,...,k und k∈IN}.<br />
Beispiele:<br />
1) Sei A={a,b} mit a
<strong>11</strong>-5<br />
<strong>der</strong> Frage, wie man Sprachen möglichst anschaulich erfassen und beschreiben kann.<br />
Zuvor wollen wir jedoch zur Erleichterung unserer Notation noch einige neue Bezeichnungen<br />
einführen.<br />
Bezeichnungen:<br />
1) Die Menge aller Wörter <strong>der</strong> Länge n über einem Alphabet A bezeichnet man mit A n :<br />
A n ={w | w∈A * und |w|=n}.<br />
Speziell gilt für n=0:<br />
A 0 ={ε}.<br />
2) Entfernt man aus A * das leere Wort, so erhält man die Menge<br />
A + =A * \{ε}.<br />
3) Sei w∈A * und n∈IN. w n bezeichnet die n-fache Aneinan<strong>der</strong>reihung von w, d.h.<br />
w n =www...ww.<br />
Speziell gilt für n=0 wie<strong>der</strong>um: w 0 =ε.<br />
<strong>11</strong>.1.2 Syntaxdiagramme<br />
Unter <strong>der</strong> Syntax einer Sprache verstehen wir die Regeln, denen die Wörter, die zur<br />
Sprache gehören, gehorchen müssen. Wir beschäftigen uns in diesem Abschnitt mit<br />
einer anschaulichen und sehr einfachen Methode, um die Syntax von Sprachen zu<br />
beschreiben, den sog. Syntaxdiagrammen.<br />
Definition E:<br />
Seien N und T Alphabete. N ist die Menge <strong>der</strong> Nichtterminalsymbole (auch Hilfszeichen<br />
o<strong>der</strong> Variablen genannt) und T die Menge <strong>der</strong> Terminalsymbole. Syntaxdiagramme<br />
über N und T sind folgen<strong>der</strong>maßen aufgebaut:<br />
1) Jedes Syntaxdiagramm ist mit einem Nichtterminalsymbol aus N markiert.<br />
2) Ein Syntaxdiagramm besteht aus Kreisen (o<strong>der</strong> Ellipsen) und Kästchen, die durch<br />
Pfeile miteinan<strong>der</strong> verbunden sind.<br />
3) In jedem Kreis (o<strong>der</strong> je<strong>der</strong> Ellipse) steht ein Wort über T. In jedem Kästchen steht ein<br />
Nichtterminalsymbol, d.h., ein Element aus N.<br />
4) Aus jedem Kreis (je<strong>der</strong> Ellipse) und jedem Kästchen führt genau ein Pfeil hinaus und<br />
genau ein Pfeil hinein.<br />
5) Ein Pfeil darf sich in mehrere Pfeile aufspalten (Verzweigungspunkt) und mehrere<br />
Pfeile dürfen zu einem Pfeil zusammengeführt werden (Zusammenfassungspunkt).<br />
6) In jedem Syntaxdiagramm gibt es genau einen ("Eingangs"-) Pfeil, <strong>der</strong> von keinem<br />
Kreis (Ellipse) o<strong>der</strong> Kästchen ausgeht (von außen in das Syntaxdiagramm einlaufen<strong>der</strong><br />
Pfeil), und genau einen ("Ausgangs"-) Pfeil, <strong>der</strong> zu keinem Kreis (Ellipse) o<strong>der</strong>
<strong>11</strong>-6<br />
Kästchen führt (nach außen aus dem Syntaxdiagramm auslaufen<strong>der</strong> Pfeil). Alle<br />
übrigen Pfeile verbinden Kästchen, Kreise (Ellipsen), Verzweigungs- und Zusammenführungspunkte.<br />
7) Von dem Eingangspfeil aus kann man jeden Kreis (Ellipse) und jedes Kästchen des<br />
Syntaxdiagramms auf mindestens einem Weg erreichen, und von jedem Kreis<br />
(Ellipse) o<strong>der</strong> Kästchen kann man auf mindestens einem Weg zum Ausgangspfeil<br />
gelangen. (Diese Bedingung besagt, daß das Syntaxdiagramm nicht in mehrere<br />
Teile zerfallen darf.)<br />
Soweit die Definition von Syntaxdiagrammen.<br />
Beispiel: Sei T={0,1,2,...,9,+,-} das Alphabet <strong>der</strong> Terminalsymbole, N={Ziffer, Ziffernfolge,<br />
ZifferohneNull, Ganzzahl} die Menge <strong>der</strong> Nichtterminalsymbole. Syntaxdiagramme über N<br />
und T zeigen Abb. 1.
<strong>11</strong>-7<br />
ZifferohneNull<br />
1 2 3 4 5 6 7 8 9<br />
Ziffer<br />
0<br />
ZifferohneNull<br />
Ziffernfolge<br />
Ziffer<br />
Ganzzahl<br />
+<br />
0<br />
-<br />
ZifferohneNull<br />
Ziffernfolge<br />
Abb.1: Beispiele für Syntaxdiagramme<br />
Syntaxdiagramme über N und T definieren eindeutig eine Sprache L⊆T * . Wie bestimmt<br />
man aber nun diese Sprache<br />
Definition F: Auswertung eines Syntaxdiagramms
<strong>11</strong>-8<br />
Man betritt das Syntaxdiagramm durch den Eingangspfeil und befolgt solange jeweils<br />
eine <strong>der</strong> folgenden Regeln, bis man das Syntaxdiagramm auf dem Ausgangspfeil verlassen<br />
hat:<br />
1) Trifft man auf einen Verzweigungspunkt, so folgt man nach Belieben einem <strong>der</strong><br />
weiterführenden Pfeile.<br />
2) Trifft man auf einen Zusammenfassungspunkt, so folgt man dem weiterführenden<br />
Pfeil.<br />
3) Trifft man auf einen Kreis (eine Ellipse), so schreibt man seinen Inhalt, also ein Wort<br />
über T, auf (bzw. hängt es hinten an das schon aufgeschriebene Wort an) und verläßt<br />
den Kreis (die Ellipse) über den ausgehenden Pfeil.<br />
4) Trifft man auf ein Kästchen, in dem ein Nichtterminalsymbol ω steht, so kopiert man<br />
das Syntaxdiagramm mit <strong>der</strong> Bezeichung ω anstelle des Kästchen ein. Der in das<br />
Kästchen hineinlaufende Pfeil wird mit dem Eingangspfeil des Syntaxdiagramms für<br />
ω und <strong>der</strong> von dem Kästchen abgehende Pfeil mit dem Ausgangspfeil des Syntaxdiagramms<br />
für ω verbunden.<br />
Die Regel 4 gilt für jedes mögliche Nichtterminalsymbol ω. Insbeson<strong>der</strong>e kann ein Syntaxdiagramm<br />
auch in sich selbst eingesetzt werden. Dadurch ist ein rekursives Einsetzen<br />
möglich!<br />
Nun wird auch klar, wie die Namen "Terminalsymbol" und "Nichtterminalsymbol" zustande<br />
kommen: Während Terminalsymbole einen Endzustand (lat.: terminare = beenden)<br />
darstellen, symbolisieren Nichtterminalsymbole einen Zwischenzustand und können<br />
weiter ersetzt werden.<br />
Definition G:<br />
Jedes Wort, das nach irgendeiner Auswertung eines Syntaxdiagramms aufgeschrieben<br />
ist, kann durch das Syntaxdiagramm erzeugt werden. Die durch das Syntaxdiagramm<br />
definierte Sprache ist die Menge aller Wörter, die auf diese Weise erzeugt werden<br />
kann.<br />
Beispiele:<br />
1) Wir betrachten noch einmal die Syntaxdiagramme aus Abb. 1.<br />
Zunächst betrachten wir das Syntaxdiagramm ZifferohneNull. Wir betreten es links<br />
durch den Eingangspfeil, gehen bei den Verzweigungspunkten z.B. zweimal geradeaus<br />
und dann nach unten. Dort treffen wir auf den Kreis mit <strong>der</strong> Inschrift 3. Wir<br />
schreiben 3 auf und verlassen auf den unten stehenden Pfeilen das Syntaxdiagramm.<br />
Die Zeichenfolge "3" ist also ein Wort <strong>der</strong> vom Syntaxdiagramm ZifferohneNull
<strong>11</strong>-9<br />
definierten Sprache. Man sieht unmittelbar, daß die Sprache, die dieses Syntaxdiagramm<br />
definiert, genau folgende endliche Menge ist:<br />
{1,2,3,4,5,6,7,8,9}.<br />
Betritt man das Syntaxdiagramm Ziffer, so kann man entwe<strong>der</strong> das Zeichen 0 hinschreiben,<br />
o<strong>der</strong> man wählt das Kästchen ZifferohneNull. Dieses Kästchen ist nun<br />
durch das Syntaxdiagramm ZifferohneNull zu ersetzen. Wie man dieses auswertet,<br />
haben wir bereits diskutiert. Die von Ziffer definierte Sprache lautet daher:<br />
{0,1,2,3,4,5,6,7,8,9}.<br />
Das Syntaxdiagramm Ziffernfolge kann man, ohne auf einen Kreis o<strong>der</strong> ein Kästchen<br />
zu treffen, wie<strong>der</strong> verlassen, d.h., das leere Wort gehört zu <strong>der</strong> hierdurch definierten<br />
Sprache. Man kann sich aber beim Verzweigungspunkt für die untere Richtung entscheiden<br />
und eine Ziffer erzeugen; dies kann man beliebig of t wie<strong>der</strong>holen. Folglich<br />
definiert das Syntaxdiagramm Ziffernfolge genau die Menge aller Wörter über den<br />
Ziffern, also<br />
{0,1,2,3,4,5,6,7,8,9} * .<br />
Man kann sich davon überzeugen, daß die durch Ganzzahl definierte Sprache die<br />
Menge aller Dezimaldarstellungen <strong>der</strong> ganzen Zahlen ist (ohne führende Nullen, ggf.<br />
mit Vorzeichen).<br />
2) Im Laufe <strong>der</strong> Vorlesung haben wir regelmäßig den Begriff des Bezeichners verwendet,<br />
ohne jemals genau zu definieren, was wir unter einem Bezeichner verstehen. Mit<br />
Hilfe von Syntaxdiagrammen können wir diese Lücke nun schließen (Abb. 2). Ein<br />
Bezeichner ist nach dieser Definition also eine Folge von Buchstaben und/o<strong>der</strong> Ziffern,<br />
die stets mit einem Buchstaben beginnt.
<strong>11</strong>-10<br />
Ziffer<br />
0<br />
1 2 3 4 5 6 7 8 9<br />
Buchstabe<br />
a<br />
...<br />
A b B z Z<br />
Bezeichner<br />
Buchstabe<br />
Buchstabe<br />
Ziffer<br />
Abb. 2: Syntaxdiagramme für Bezeichner<br />
Wir haben Syntaxdiagramme eingeführt, um Sprachen präzise definieren können. Dabei<br />
haben wir aber noch nicht überprüft, ob Syntaxdiagramme ein genügend mächtiges<br />
Darstellungsmittel in dem Sinne sind, daß man jede beliebige Sprache L⊆T * über einem<br />
Alphabet T mit ihnen beschreiben kann. Wir wollen hier ein Ergebnis vorwegnehmen,<br />
welches in Vorlesungen über Formale Sprachen erarbeitet wird. Man kann beweisen,<br />
daß mit Syntaxdiagrammen aus theoretischer Sicht zwar nur relativ eingeschränkte,<br />
aber gerade die für die Praxis sehr interessanten Sprachen beschrieben werden<br />
können, die sog. kontextfreien Sprachen. Die Syntax sehr vieler bekannter <strong>Programmiersprachen</strong><br />
(auch die von uns benutzten <strong>Programmiersprachen</strong> PRO und ASS) fällt im<br />
wesentlichen (Präzisierung: siehe Vorlesungen über Theoretische Informatik) unter die
<strong>11</strong>-<strong>11</strong><br />
kontextfreien Sprachen. Daß es aber auch sehr einfache Sprachen gibt, die nicht durch<br />
Syntaxdiagramme definiert werden können, zeigt folgendes<br />
Beispiel: T sei die Menge { | }. Man kann zeigen, daß die Sprache L⊆T * , die genau alle<br />
Folgen mit quadratisch vielen Strichen enthält, also<br />
L={ | n | n ist Quadratzahl},<br />
nicht durch ein Syntaxdiagramm definiert werden kann.<br />
<strong>11</strong>.1.3 Backus-Naur-Form (BNF)<br />
Eine an<strong>der</strong>e nicht-graphische Methode zur Darstellung von Sprachen ist die Backus-<br />
Naur-Form, die von den beiden Wissenschaftlern J. Backus und P. Naur vor über 30<br />
Jahren vorgeschlagen und von einem Komitee zur Definition <strong>der</strong> Programmiersprache<br />
ALGOL 60 verwendet wurde.<br />
Dieser Ansatz verläuft, vereinfacht gesprochen, so: Man beginnt mit einer Zeichenfolge<br />
und erzeugt durch eine Folge von Verän<strong>der</strong>ungsschritten potentiell jedes mögliche Wort<br />
<strong>der</strong> Sprache. Die Regeln, nach denen Verän<strong>der</strong>ungen an Zeichenfolgen vorgenommen<br />
werden dürfen, bezeichnet man als Grammatik. Es ist klar, daß wir zur Notation des<br />
Regelwerks – wie schon bei Syntaxdiagrammen – spezielle Symbole benötigen, die mit<br />
den Zeichen <strong>der</strong> Sprache, die wir definieren wollen, nicht verwechselt werden können.<br />
Fachmännisch gesprochen: Wir benötigen eine Metasprache, d.h. eine Sprache, mit <strong>der</strong><br />
wir eine an<strong>der</strong>e Sprache mit Hilfe einer Grammatik definieren können. Eine solche<br />
Metasprache ist die Backus-Naur-Form.<br />
Wie schon bei Syntaxdiagrammen unterscheidet man bei einer Grammatik in Backus-<br />
Naur-Form zwei Zeichenmengen: ein Alphabet N von Nichtterminalsymbolen und ein<br />
Alphabet T von Zeichen <strong>der</strong> zu definierenden Sprache, die Terminalsymbole.<br />
Jedes Nichtterminalsymbol ist in spitze Klammern eingeschlossen, die nicht als Terminalsymbole<br />
vorkommen dürfen. Dies hat den Zweck, Nichtterminalsymbole kenntlich<br />
zu machen, denn sie sind ja meist mit Hilfe des gleichen Alphabets geschrieben wie die<br />
Sprache selbst (nämlich im Alphabet <strong>der</strong> deutschen Sprache). In <strong>der</strong> Menge <strong>der</strong> Nichtterminalsymbole<br />
zeichnet man ein Symbol S beson<strong>der</strong>s aus, das Startsymbol.<br />
Das Startsymbol ist gewissermaßen <strong>der</strong> allgemeinste Begriff, d.h., die Zeichenfolge, bei<br />
<strong>der</strong> man mit <strong>der</strong> Durchführung von Verän<strong>der</strong>ungsschritten beginnt. Die Verän<strong>der</strong>ungsregeln<br />
selbst bezeichnet man als Regeln o<strong>der</strong> Produktionen. Sie werden in <strong>der</strong> Backus-<br />
Naur-Form folgen<strong>der</strong>maßen aufgeschrieben:<br />
X ::= w 1 | w 2 | ... | w n .
<strong>11</strong>-12<br />
Dabei ist X∈N stets ein einzelnes (in spitze Klammern gefaßtes) Nichtterminalsymbol.<br />
w 1 ,w 2 ,...,w n ∈(N∪T) * sind n beliebige Wörter, die aus Nichtterminal- und Terminalsymbolen<br />
gebildet sind (auch das leere Wort ε ist möglich). Die obige Produktion bedeutet:<br />
Wenn in einem Wort an irgendeiner Stelle das Nichtterminalsymbol<br />
X vorkommt, so darf es durch eines <strong>der</strong> Worte w 1 ,w 2 , ...,w n ersetzt werden.<br />
Damit dieser Ersetzungsschritt problemlos durchgeführt werden kann, dürfen die Symbole<br />
"::=" und "|" nicht als Terminalsymbole vorkommen. Sie gehören zur Metasprache.<br />
Das Viertupel G=(N,T,P,S), bestehend aus <strong>der</strong> Menge <strong>der</strong> Nichtterminalsymbole N, <strong>der</strong><br />
Menge <strong>der</strong> Terminalsymbole T, <strong>der</strong> Menge <strong>der</strong> Produktionen P und dem Startsymbol S<br />
bezeichnet man als Grammatik in Backus-Naur-Form (BNF-Grammatik).<br />
Beispiel: Die folgende BNF-Grammatik entspricht den Syntaxdiagrammen in Abb. 1. Es<br />
sei<br />
T={0,1,2,...,9,+,-},<br />
N={,,,}.<br />
Das Startsymbol S sei das Symbol . Die Produktionen seien definiert durch:<br />
::= 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9<br />
::= 0 | <br />
::= ε | <br />
::= 0 | +0 | -0 | |<br />
+ |<br />
- <br />
Definition H:<br />
Eine Grammatik in Backus-Naur-Form, kurz BNF-Grammatik, ist ein Viertupel<br />
G=(N,T,P,S) mit<br />
a) T ist die Menge <strong>der</strong> Terminalsymbole, N die Menge <strong>der</strong> Nichterminalsymbole<br />
und T∩N=∅;<br />
b) S∈N ist das Startsymbol;<br />
c) P ist die Menge <strong>der</strong> Produktionen. Eine Produktion p∈P ist eine Zeichenfolge <strong>der</strong><br />
Form<br />
X ::= w 1 | w 2 | ... | w n .<br />
wobei X∈N, n≥1 und w i ∈(N∪T) * für i=1,2,...,n sind.<br />
Wie kann man nun mit Hilfe einer BNF-Grammatik aus dem Startsymbol Wörter erzeugen,<br />
und was ist die von <strong>der</strong> Grammatik definierte Sprache Wie bereits kurz angeschnitten,<br />
beginnt man mit dem Startsymbol S. Wann immer ein Wort über T∪N, also
<strong>11</strong>-13<br />
bestehend aus Terminal- und Nichtterminalsymbolen, vorliegt, in dem das Nichtterminalsymbol<br />
X vorkommt, darf man X durch eines <strong>der</strong> Wörter w 1 ,w 2 ,...,w n ersetzen, sofern<br />
X::=w 1 | w 2 | ... | w n eine Produktion <strong>der</strong> Grammatik ist. Die Ersetzungsschritte brechen<br />
ab, wenn man ein Wort über T erhalten hat, welches also nur noch aus Terminalsymbolen<br />
besteht.<br />
Definition I:<br />
Sei G=(N,T,P,S) eine BNF-Grammatik.<br />
a) Ein Wort v∈(N∪T) * heißt in einem Schritt aus einem Wort u∈(N∪T) * ableitbar,<br />
wenn es Wörter u 1 ,u 2 ∈(N∪T) * , ein X∈N und eine Produktion <strong>der</strong> Form<br />
X::=w 1 |w 2 |...|w n in P gibt, so daß für ein i∈{1,...,n} gilt:<br />
u=u 1 Xu 2 und v=u 1 wu 2 .<br />
Man schreibt dann kurz u→v.<br />
b) Ein Wort v∈(N∪T) * heißt ableitbar aus einem Wort u∈(N∪T) * , wenn u=v ist o<strong>der</strong><br />
wenn es Wörter u=u 0 ,u 1 ,u 2 ,...,u r =v∈(N∪T) * gibt, so daß<br />
u i-1 →u i für i∈{1,..., r}<br />
gilt. Man schreibt dann kurz u→ * v.<br />
u 0 →u 1 →u 2 →...→ u r heißt Ableitung <strong>der</strong> Länge r von u 0 nach u r .<br />
c) Die von einer BNF-Grammatik G erzeugte Sprache ist definiert durch<br />
L(G)={w∈T * | S→ * w}.<br />
L(G) ist also die Menge aller aus dem Startsymbol ableitbaren Wörter, die nur aus<br />
Terminalsymbolen bestehen.<br />
Beispiele:<br />
1) Man betrachte obiges Beispiel. Frage: Ist das Wort 732 aus dem Startsymbol ableitbar<br />
Ja, denn man kann folgende Ableitung bilden:<br />
→ →<br />
→<br />
7 →<br />
7 →<br />
7 →<br />
7 → 73 →<br />
73 → 732.<br />
Die von <strong>der</strong> BNF-Grammatik G erzeugte Sprache L(G) ist wie beim zugehörigen<br />
Syntaxdiagramm die Menge <strong>der</strong> Dezimaldarstellungen <strong>der</strong> ganzen Zahlen ohne<br />
führende Nullen und ggf. mit Vorzeichen.<br />
2) Sei G=(N,T,P,) eine BNF-Grammatik mit N={}, T={a,b},
<strong>11</strong>-14<br />
P={ ::= ε | a | b}.<br />
Mit G kann man jede beliebige Folge von a und b erzeugen, d.h. L(G)=T * .<br />
3) Sei G=(N,T,P,S) eine BNF-Grammatik mit N={}, T={(,)},<br />
P={::=() | ε}.<br />
Mit G kann man alle korrekten Klammerungen erzeugen, d.h., alle Klammerfolgen,<br />
bei denen an je<strong>der</strong> Stelle <strong>der</strong> Folge mehr Klammern geöffnet als geschlossen wurden<br />
und zusätzlich am Ende <strong>der</strong> Folge die Zahl <strong>der</strong> geöffneten Klammern gleich <strong>der</strong><br />
Zahl <strong>der</strong> geschlossenen Klammern ist. Zum Beispiel ist ((()())()) eine korrekte,<br />
((())()))())) eine unkorrekte Klammerung.<br />
Eine Ableitung soll die Wirkungsweise verdeutlichen:<br />
→ ( ) → (()) →<br />
(( ))() → (() )() →<br />
(())( ) → (())() → (())()<br />
Dabei ist in jedem zwischenzeitlich abgeleiteten Wort das Nichtterminalsymbol<br />
unterstrichen, das im nächsten Schritt ersetzt wird.<br />
4) Die folgende Grammatik G=(N,T,P,) mit<br />
N={,,,},<br />
T={a,b,...,z,0,1,...,9} und<br />
P={::= | ,<br />
::= | |<br />
|<br />
,<br />
::= 0 | 1 | ... | 9,<br />
::= a | b | ... | z}<br />
definiert die Sprache <strong>der</strong> Wörter über T, die als Bezeichner verwendet werden<br />
dürfen.<br />
BNF-Grammatiken dienen in <strong>der</strong> Informatik wie Syntaxdiagramme meist dazu, die Syntax<br />
von <strong>Programmiersprachen</strong> zu definieren.<br />
Für bestimmte Produktionen hat man in <strong>der</strong> Backus-Naur-Form Abkürzungen eingeführt.<br />
1. Abkürzung: Symbole o<strong>der</strong> Symbolfolgen innerhalb einer Produktion, die auch weggelassen<br />
werden können, werden in eckige Klammern eingeschlossen.<br />
Beispiel: Eine Produktion <strong>der</strong> Form<br />
A ::= B C | B<br />
wobei B und C beliebige Symbolfolgen sind, kann man abkürzen durch<br />
A ::= B [C].
<strong>11</strong>-15<br />
Die Produktion für in obigem Beispiel 4 schreibt man dann entsprechend<br />
als<br />
::= []<br />
2. Abkürzung: Symbole o<strong>der</strong> Symbolfolgen, die beliebig oft wie<strong>der</strong>holt o<strong>der</strong> aber auch<br />
weggelassen werden können, werden in geschweifte Klammern eingeschlossen.<br />
Beispiel: Anstelle <strong>der</strong> Produktionen für und in Beispiel 4 kann<br />
man nun kurz schreiben:<br />
::= { | }<br />
Welche Mächtigkeit besitzen nun BNF-Grammatiken im Vergleich zu Syntaxdiagrammen<br />
Man kann zeigen, daß man zu je<strong>der</strong> BNF-Grammatik ein gleichwertiges Syntaxdiagramm<br />
konstruieren kann und umgekehrt. Den Beweis werden wir hier nicht führen;<br />
er ist relativ einfach, und man kann ihn sich an Beispielen leicht klar machen. Man kann<br />
also mit BNF-Grammatiken die gleichen Sprachen definieren wie mit Syntaxdiagrammen.<br />
Diese Sprachen nennt man die kontextfreien Sprachen.<br />
Anhand <strong>der</strong> Backus-Naur-Form von Grammatiken läßt sich gut nachvollziehen, was <strong>der</strong><br />
Begriff "kontextfrei" bedeutet. Er bedeutet, daß innerhalb eines Ableitungsprozesses<br />
jedes Nichtterminalzeichen ersetzt werden darf, unabhängig davon, in welchem<br />
Kontext es auftritt, d.h., welche Zeichen rechts o<strong>der</strong> links neben stehen.<br />
Da alle bekannten <strong>Programmiersprachen</strong> im wesentlichen zu den kontextfreien Sprachen<br />
gehören, haben Syntaxdiagramme wie auch BNF-Grammatiken eine große Bedeutung.<br />
Doch Vorsicht! Wir haben gesagt "im wesentlichen" und meinen damit, daß<br />
gewisse, aber wenige syntaktische Regeln einer Programmiersprache nicht mit Hilfe<br />
eines Syntaxdiagramms o<strong>der</strong> einer BNF-Grammatik definiert werden können. Zwei dieser<br />
Regeln sind z.B.<br />
- Je<strong>der</strong> Bezeichner, <strong>der</strong> in einem Programm verwendet wird, muß vorher deklariert<br />
werden.<br />
- Die Anzahl <strong>der</strong> in <strong>der</strong> Deklaration einer Prozedur auftretenden formalen Parameter ist<br />
gleich <strong>der</strong> Zahl <strong>der</strong> bei <strong>der</strong> Anwendung <strong>der</strong> Prozedur aufgeführten aktuellen Parameter.<br />
Man kann zeigen, daß keine dieser beiden Regeln durch ein Syntaxdiagramm o<strong>der</strong> eine<br />
BNF-Grammatik erzwungen werden kann. Trotz dieses Mangels definiert man <strong>Programmiersprachen</strong><br />
stets durch Syntaxdiagramme o<strong>der</strong> BNF-Grammatiken. Regeln <strong>der</strong> obigen<br />
Form, die man hiermit nicht beschreiben kann, fügt man umgangssprachlich hinzu.
<strong>11</strong>-16<br />
<strong>11</strong>.2 Semantik<br />
Wie zu Beginn dieses <strong>Kapitel</strong>s erwähnt ist eine Programmiersprache P ein Paar P=(L,F)<br />
mit <strong>der</strong> formalen Sprache L⊆A * , die die Syntax von P bestimmt, und <strong>der</strong> Abbildung F, die<br />
jedem Programm w∈L seine Semantik zuordnet. Mit <strong>der</strong> Syntax haben wir uns soeben<br />
beschäftigt. Wie kann man nun die Semantik eines Programms präzisieren<br />
Schon früher (Abschnitt 4.5.1) hatten wir einem Programm mit <strong>der</strong> Methode <strong>der</strong> denotationalen<br />
Semantik die von w berechnete Funktion<br />
f w : I→O<br />
von <strong>der</strong> Menge I <strong>der</strong> Eingabedaten in die Menge O <strong>der</strong> Ausgabedaten als Bedeutung<br />
zuordnet. F ist nach diesem Ansatz also eine Funktion<br />
F: A * →(I→O) mit<br />
f w , falls w∈L<br />
F[w]=<br />
⊥, sonst.<br />
F nennt man auch semantische Funktion. F liefert also zu jedem syntaktisch korrekten<br />
Programm als seine Bedeutung die zugehörige berechnete Funktion. Programme,<br />
die syntaktisch fehlerhaft sind, haben keine Bedeutung, und die semantische Funktion<br />
ist hierfür undefiniert.<br />
Beispiel: Gegeben sei folgende Zeichenfolge w mit<br />
w ≡ funktion f x:int → int ≡<br />
wenn x=1 dann 2 sonst 2*(f(x-1)).<br />
Offenbar ist w ein syntaktisch korrektes Programm. Wie lautet die Semantik von w Es<br />
gilt:<br />
F[w]=exp2 mit<br />
2 x , falls x≥1<br />
exp2(x)=<br />
⊥, sonst.<br />
w berechnet also die Zweierpotenz, wenn man natürliche Zahlen ≥1 eingibt. In den übrigen<br />
Fällen terminiert w nicht, und die berechnete Funktion ist undefiniert.<br />
Die Aufgabe besteht nun darin, F für alle Programme w einer Programmiersprache<br />
P=(L,F) zu definieren. Natürlich kann man dies nicht für die unendlich vielen w explizit<br />
durchführen, vielmehr muß man den speziellen syntaktischen Aufbau von w ausnutzen<br />
und F induktiv über diesen Aufbau definieren. Man präzisiert also die Semantik aller<br />
Sprachelemente isoliert voneinan<strong>der</strong>, zunächst <strong>der</strong> elementaren Sprachelemente, dann<br />
<strong>der</strong> einzelnen Konstruktoren usw. Schließlich liegt für jedes Sprachelement das partielle<br />
Verhalten <strong>der</strong> semantischen Funktion vor, und man kann für ein konkretes Programm
<strong>11</strong>-17<br />
alle diese Teilfunktionen entsprechend des Programmaufbaus zusammensetzen und<br />
erhält so die Semantik des Gesamtprogramms.<br />
Um die prinzipielle Vorgehensweise bei <strong>der</strong> Definition einer semantischen Funktion F zu<br />
verstehen, beschränken wir uns jedoch <strong>der</strong> Übersichtlichkeit halber im weiteren Verlauf<br />
auf die Semantikdefinition einer Teilmenge von ML, genannt µML.<br />
<strong>11</strong>.2.1 Die Syntax von µML<br />
In µML mögen nur folgende Typen und Konstrukte von µML zugelassen sein:<br />
<strong>der</strong> elementare Datentyp int,<br />
Ausdrücke über int und bool,<br />
Wertdeklarationen <strong>der</strong> Form val x=E mit einem arithmetischen<br />
Ausdruck E vom Typ int,<br />
Funktionsdefinitionen <strong>der</strong> Form fun f x =... mit einem Parameter,<br />
eine spezielle Eingabeanweisung read.<br />
Die Syntax von µML lautet in Backus-Naur-Form:<br />
::= ; <br />
::= ; | ε<br />
::= | <br />
::= val = <br />
::= fun = <br />
::= | <br />
::= ( ) | |<br />
| ()<br />
::= + | - | * | /<br />
::= if then else <br />
::= = | ≠ | true | false<br />
::= "Bezeichner nach üblichen Konventionen"<br />
::= "ganzzahlige Konstante"<br />
::= (read(); ).<br />
<strong>11</strong>.2.2 Umgebungen<br />
Die Semantik etwa eines arithmetischen Ausdrucks ist sein Wert. Um also die Semantik<br />
eines Ausdrucks z.B. <strong>der</strong> Form<br />
2+x
<strong>11</strong>-18<br />
zu ermitteln, müssen die Werte aller Objekte des Ausdrucks entsprechend <strong>der</strong> beteiligten<br />
Operationen verknüpft werden. Die Werte von Konstanten sind unmittelbar abzulesen,<br />
wie ermittelt man jedoch die Werte von Bezeichnern Offenbar muß man eine Art<br />
Symboltabelle mitführen, aus <strong>der</strong> man die aktuelle Bindung jedes Bezeichners an je<strong>der</strong><br />
Programmstelle ablesen kann. Da sich Bindungen im Laufe eines Programms, etwa<br />
durch Neudefinition, än<strong>der</strong>n können, benötigt man ferner Operationen, um den Inhalt <strong>der</strong><br />
Symboltabelle ebenfalls än<strong>der</strong>n zu können.<br />
Diese Überlegungen führen auf den Begriff <strong>der</strong> sog. Umgebung.<br />
Umgebungen u sind Abbildungen von <strong>der</strong> Menge <strong>der</strong> möglichen Bezeichner in die<br />
Menge <strong>der</strong> möglichen Werte; in µML sind das einerseits die elementaren Werte und<br />
an<strong>der</strong>erseits Funktionsrümpfe (=Ausdrücke). Wir definieren also:<br />
u: {Bezeichner} → int∪({Bezeichner}×[int→int]×{Umgebungen}).<br />
Zu jedem Bezeichner x liefert u den aktuellen Wert u(x), sofern x gebunden ist,<br />
an<strong>der</strong>enfalls undefiniert ⊥. Ist x an einen elementaren Wert gebunden, so ist u(x) dieser<br />
Wert, ist x an eine Funktion gebunden, so ist u(x) ein Tripel<br />
(p,r,u').<br />
Hierbei ist p <strong>der</strong> Bezeichner des formalen Parameters von x, r <strong>der</strong> Funktionsrumpf von x<br />
(also ein Ausdruck, <strong>der</strong> eine Funktion in [int→int] beschreibt) und u' die Umgebung, in<br />
<strong>der</strong> x deklariert wurde. u' wird benötigt, um das Konzept <strong>der</strong> statischen Bindung von ML<br />
formal korrekt zu formulieren: Wird nämlich x aufgerufen, so gelten für die Objekte im<br />
Rumpf von x die Bindungen u' zum Zeitpunkt <strong>der</strong> Deklaration von x und nicht die Bindungen<br />
u zum Zeitpunkt des Aufrufs von x. Die Umgebung u' muß daher in dem Augenblick<br />
wie<strong>der</strong>hergestellt werden, wenn x aufgerufen wird.<br />
Für später legen wir folgende Bezeichnungen fest: Zu einem Namen x, <strong>der</strong> in <strong>der</strong><br />
aktuellen Umgebung u eine Funktion bezeichnet, also u(x)=(p,r,u'), liefere π den formalen<br />
Parameter p, also π(u(x))=p, ρ den Rumpf <strong>der</strong> Funktion, also ρ(u(x))=r, und υ die<br />
Umgebung, in <strong>der</strong> x deklariert wurde, also υ(u(x))=u'.<br />
Beispiel: Definiert man in <strong>der</strong> Umgebung u eine Funktion<br />
fun f p = if B then E1 else E2,<br />
so erhält man danach eine neue Umgebung u', in <strong>der</strong> für f gilt:<br />
u'(f)=(p, if B then E1 else E2, u).<br />
Umgebungen stellen wir meist als Menge von Gleichungen<br />
u={x 1 =w 1 ,...,x n =w n }<br />
dar. Hierbei bedeutet x i =w i , daß <strong>der</strong> Bezeichner x i in <strong>der</strong> Umgebung u den Wert w i<br />
besitzt. Für y≠x i , i=1,...,n gilt u(y)=⊥.
<strong>11</strong>-19<br />
Im Laufe eines Programms können Bezeichner an unterschiedliche Werte gebunden<br />
werden. Um diese Verän<strong>der</strong>ungen von Umgebungen übersichtlich formal zu beschreiben,<br />
definieren wir eine Operation + auf Umgebungen wie folgt: Sei<br />
u={x 1 =w 1 ,...,x n =w n } und<br />
u'={y 1 =v 1 ,...,y m =v m }.<br />
Dann gilt:<br />
u"=u+u' mit<br />
u(x), falls u'(x)=⊥,<br />
u"(x)=<br />
u'(x), sonst.<br />
In u" werden also die Bindungen <strong>der</strong> Bezeichner in u durch die Bindungen in u' überschrieben.<br />
Man beachte, daß + nicht kommutativ ist.<br />
Beispiel: Sei u={x=2, y=3}, u'={x=(a, if a=0 then 0 else 1,{y=4,z=0}), z=5}. Dann ist<br />
u"=u+u'={x=(a, if a=0 then 0 else 1,{y=4,z=0}), y=3, z=5}.<br />
Nun wollen wir die Semantik von µML definieren. Dabei gehen wir wie erwähnt induktiv<br />
vor, indem wir für die einzelnen Sprachelemente von µML semantische Funktionen<br />
definieren, die im Zusammenspiel geeignet sind, die Semantik von Programmen zu<br />
beschreiben.<br />
Programme.<br />
Wir beginnen bei Programmen und brechen die zugehörige semantische Funktion<br />
schrittweise auf semantische Funktionen für die Bestandteile <strong>der</strong> Programme herunter.<br />
Die Bedeutung von Programmen beschreibt die semantische Funktion:<br />
B: µML→[int*→int].<br />
B bildet also wie erwartet Programmtexte in µML auf Funktionen ab, die einen Eingabeund<br />
einen Ausgabewert vom Typ int besitzen. B ist definiert durch<br />
B[;(read();)](α)=<br />
E[]((D[]∅)+{=α}).<br />
Erläuterung: Die Bedeutung eines Programms <strong>der</strong> angegebenen Form<br />
;(read();)<br />
mit dem Eingabewert α erhält man wie folgt: Man stellt zunächst mittels D die Umgebung<br />
her, die die Deklarationen auf <strong>der</strong> leeren Umgebung ∅ (noch kein Bezeichner<br />
gebunden) bewirken. Zu dieser Umgebung fügt man die Bindung des aktuellen Eingabewertes<br />
α an den Bezeichner hinzu. In dieser neuen Umgebung wertet man anschließend<br />
mittels <strong>der</strong> semantischen Funktion E den Ausdruck aus und erhält<br />
das Ergebnis. D und E beschreiben wir im folgenden.
<strong>11</strong>-20<br />
Deklarationen.<br />
Die Bedeutung von Deklarationen beschreibt die semantische Funktion<br />
D: {Deklarationen}→[{Umgebungen}→{Umgebungen}].<br />
Eine Folge von Deklarationen bildet also unter D Umgebungen auf neue Umgebungen<br />
ab. D ist definiert durch:<br />
D[ ]u=u,<br />
D[;]=D[] ° D[].<br />
Erläuterung: Bilde zu einer Umgebung zunächst die neue Umgebung, die durch die<br />
Deklaration hergestellt wird, und wende darauf die Umgebungstransformation<br />
von an. Auf diese Weise werden die einzelnen Deklarationen schrittweise<br />
abgearbeitet und die jeweiligen Umgebungen aufgebaut.<br />
Wertdeklarationen:<br />
D[ val =]u=u+{=E[]u}.<br />
Erläuterung: Eine Wertdeklaration verän<strong>der</strong>t eine vorliegende Umgebung u durch<br />
Hinzufügen <strong>der</strong> Bindung von an den mittels <strong>der</strong> semantischen Funktion E (s.u.) in<br />
<strong>der</strong> Umgebung u bestimmten Wert des Ausdrucks .<br />
Funktionsdeklarationen:<br />
D[ fun = ]u=u+{=(,,u)},<br />
Erläuterung: Eine Funktionsdeklaration verän<strong>der</strong>t eine Umgebung u durch Hinzufügen<br />
<strong>der</strong> Bindung von an das Tripel bestehend aus dem formalem Parameter ,<br />
dem Funktionsrumpf und <strong>der</strong> Umgebung, in <strong>der</strong> deklariert wurde. Dies ist u<br />
selbst.<br />
Ausdrücke.<br />
Die Bedeutung von Ausdrücken wird durch die semantische Funktion<br />
E: {Ausdrücke}→[{Umgebungen}→int∪bool]<br />
beschrieben: Sie wertet einen Ausdruck in einer Umgebung aus und liefert einen Wert<br />
aus int o<strong>der</strong> bool. Man definiert sie induktiv.<br />
Für arithmetische Ausdrücke:<br />
E[( + )]u=(E[]u + E[]u),<br />
↑ ↑<br />
Beachte: Hier handelt es sich um ein Funktionszeichen, hier um die zugehörige Funktion.<br />
Analog lautet die Definition für die Operationen -,*,/.<br />
Für boolesche Ausdrücke:<br />
E[ = ]u=E[]u = E[]u,<br />
E[ ≠ ]u=E[]u ≠ E[]u,
<strong>11</strong>-21<br />
E[true]u=wahr,<br />
E[false]u=falsch.<br />
Für bedingte Ausdrücke:<br />
E[ if then else ]u=<br />
E[]u, falls E[]u=wahr,<br />
E[]u, falls E[]u=falsch.<br />
Für Konstanten:<br />
E[]u=.<br />
Für Bezeichner:<br />
E[]u=u().<br />
Für Funktionsanwendungen:<br />
E[()]u=<br />
E[ρ(u())](D[ fun (π(u()))=ρ(u());<br />
val π(u())=E[]u]υ(u())),<br />
Erläuterung: Die Semantik eines Funktionsaufrufs erhält man wie folgt: Man wertet den<br />
Funktionsrumpf ρ(u()) aus. Das muß in <strong>der</strong> Umgebung υ(u()) geschehen, in <strong>der</strong><br />
die Funktion deklariert wurde. Diese Umgebung reichert man mittels D einerseits<br />
an um die Funktionsdeklaration fun ... selbst, denn muß ja innerhalb von <br />
bekannt sein (um Rekrusionen zu ermöglichen). An<strong>der</strong>erseits ergänzt man die Bindung<br />
des aktuellen Parameters , <strong>der</strong> in <strong>der</strong> Umgebung u, also vor Aufruf, mittels<br />
E[]u ausgewertet wird, an den formalen Parameter π(u()), d.h. man<br />
deklariert den formalen Parameter in <strong>der</strong> Umgebung υ(u()) neu.<br />
Für boolesche Ausdrücke:<br />
E[ = ]u=E[]u = E[]u,<br />
E[ ≠ ]u=E[]u ≠ E[]u,<br />
E[true]u=wahr,<br />
E[false]u=falsch.<br />
Die Funktionen D[a], E[a] usw. heißen auch Denotationen von a. Die Funktionensemantik<br />
heißt danach auch denotationale Semantik.<br />
Beispiele:<br />
1) Wir bestimmen die Semantik eines einfachen Programms P. P sei<br />
val x=1;<br />
val y=3;<br />
(read(z); (y*(z+x))).<br />
Für einen beliebigen Eingabewert α:int gilt:
<strong>11</strong>-22<br />
B[P](α)=E[(y*(z+x))]((D[ val x=1; val y=3]∅)+{z=α}).<br />
Hierbei ist<br />
D[ val x=1; val y=3]=D[ val y=3] ° D[ val x=1].<br />
Im einzelnen:<br />
D[ val x=1]∅=∅+{x=E[1]∅}={x=1},<br />
D[ val y=3]{x=1}={x=1}+{y=3}={x=1,y=3}.<br />
Folglich gilt:<br />
B[P](α)=E[(y*(z+x))] {x=1,y=3,z=α}.<br />
Setzen wir u={x=1,y=3,z=α}, so folgt:<br />
E[(y*(z+x))]u=(E[y]u*E[(z+x)]u)=(E[y]u*(E[z]u+E[x]u))<br />
=(u(y)*(u(z)+u(x)))=(3*(α+1)).<br />
Damit ist bewiesen, daß die Semantik des Programms P, dargestellt durch die<br />
berechnete Funktion, lautet:<br />
B[P](α)=3*(α+1).<br />
2) Wir bestimmen die Semantik von P':<br />
val c=1;<br />
fun f x = if x=0 then c else f(x-1);<br />
(read(x);f(x)).<br />
Für einen beliebigen Eingabewert α:int gilt:<br />
B[P'](α)=E[f(x)]((D[ val c=1; fun f x= if x=0 then c else f(x-1)]∅)+{x=α}).<br />
Hierbei ist<br />
D[ val c=1; fun f x= if x=0 then c else f(x-1)]=<br />
D[ fun f(x)= if x=0 then c else f(x-1)] ° D[ val c=1].<br />
Im einzelnen:<br />
D[ val c=1]∅=∅+{c=E[1]∅}={c=1},<br />
D[ fun f x= if x=0 then c else f(x-1)]{c=1}=<br />
{c=1}+{f=(x, if x=0 then c else f(x-1),{c=1})}=<br />
{c=1,f=(x, if x=0 then c else f(x-1),{c=1})}.<br />
Folglich gilt:<br />
B[P'](α)=E[f(x)] {x=α, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}.<br />
Setzen wir u={x=α, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}, so folgt<br />
E[f(x)]u=E[ if x=0 then c else f(x-1)]<br />
(D[ fun f x= if x=0 then c else f(x-1); val x=E[x]u]υ(u(f)))=<br />
E[ if x=0 then c else f(x-1)]<br />
(D[ fun f x= if x=0 then c else f(x-1); val x=E[x]u]{c=1})=<br />
(*) E[ if x=0 then c else f(x-1)]{x=α, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}=<br />
E[c]u=u(c)=1, falls E[x=0]u=wahr, d.h., falls u(x)=0,
<strong>11</strong>-23<br />
=<br />
E[f(x-1)]u, sonst.<br />
Hierbei ist<br />
(**) E[f(x-1)]u=E[ if x=0 then c else f(x-1)]<br />
(D[ fun f x= if x=0 then c else f(x-1); val x=E[x-1]u]υ(u(f)))=<br />
E[f(x-1)]u=E[ if x=0 then c else f(x-1)]<br />
(D[ fun f x= if x=0 then c else f(x-1); val x=E[x-1]u]{c=1})=<br />
E[ if x=0 then c else f(x-1)]{x=α-1, c=1, f=(x, if x=0 then c else f(x-<br />
1),{c=1})}.<br />
Nun betrachte man die Zeilen (*) und (**), die zu folgen<strong>der</strong> Gleichung führen:<br />
(***) =<br />
E[ if x=0 then c else f(x-1)]u=<br />
1, falls u(x)=0,<br />
E[ if x=0 then c else f(x-1)] {x=α-1, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}, sonst.<br />
Setzt man nun zur Abkürzung<br />
F:= E[ if x=0 then c else f(x-1)],<br />
G:= D[ val x=x-1],<br />
u:= {x=α, c=1, f=(x, if x=0 then c else f(x-1),{c=1})} (wie oben),<br />
so erhält man aus (***) folgende Funktionalgleichung<br />
1, falls u(x)=0,<br />
F(u)=<br />
F(G(u)), sonst.<br />
Gesucht ist in dieser Gleichung die Funktion F, welche die Semantik des Programmstücks<br />
if x=0 then c else f(x-1)<br />
beschreibt; G und u sind bekannt. Die semantische Analyse des Programms P' hat<br />
also nicht – wie in Beispiel 1 – zu einer expliziten Angabe <strong>der</strong> berechneten Funktion<br />
von P' geführt. Vielmehr haben wir nun eine Funktionsgleichung vorliegen, die erst<br />
aufzulösen ist.<br />
Da es sich bei <strong>der</strong> obigen Gleichung für F um eine implizite Gleichung handelt, kann<br />
man sich fragen, ob es überhaupt Funktionen F gibt, die diese Gleichung erfüllen.<br />
Dies ist gleichbedeutend mit <strong>der</strong> Frage, ob man dem analysierten Programm in dem<br />
entwickelten Kalkül <strong>der</strong> semantischen Funktionen tatsächlich eine Bedeutung zuordnen<br />
kann.
<strong>11</strong>-24<br />
Analysiert man die Gleichung genauer, so stellt sich heraus, daß es sich um eine<br />
sog. Fixpunktgleichung 0 <strong>der</strong> Form<br />
F=τ(F)<br />
handelt, wobei τ ein Funktional ist mit<br />
1, falls u(x)=0,<br />
τ(F)(u)=<br />
F(G(u)), sonst.<br />
Die Lösung F dieser Fixpunktgleichung ist dann die gesuchte Semantik des Programms.<br />
Die obige Frage nach <strong>der</strong> Lösung <strong>der</strong> Gleichung ist damit auf das Problem<br />
reduziert, unter welchen Bedingungen solche Fixpunkte existieren und ob sie ggf.<br />
eindeutig bestimmt sind. Die Eindeutigkeit ist beson<strong>der</strong>s wichtig, denn die Semantik<br />
des Programmstücks (<strong>der</strong> Fixpunkt) sollte ja auf keinen Fall mehrdeutig sein. Ferner<br />
sollte <strong>der</strong> Fixpunkt auch berechenbar sein.<br />
Diesem Problemkreis, dessen Lösung einen relativ hohen technischen Aufwand erfor<strong>der</strong>t,<br />
können wir uns in dieser Vorlesung nicht widmen. Wir wollen uns stattdessen mit<br />
einem kurzen Abriß <strong>der</strong> Vorgehensweise begnügen, wie man den gesuchten Fixpunkt<br />
berechnet, ohne jedoch die zugrundeliegenden mathematischen Strukturen darzustellen<br />
o<strong>der</strong> das Verfahren Fixpunktberechnungsverfahren beweistechnisch herzuleiten.<br />
Dies bleibt Vorlesungen über die Theorie <strong>der</strong> Programmierung vorbehalten.<br />
<strong>11</strong>.2.3 Fixpunktberechnung<br />
Wie gesehen ordnet die Funktionensemantik den Programmen einer Programmiersprache<br />
Funktionen F als Bedeutung zu, die teilweise – wie in Beispiel 2 aus <strong>11</strong>.2.2 –<br />
durch implizite Fixpunktgleichungen <strong>der</strong> Form F=τ(F) bestimmt sind. Wie geht man vor,<br />
um diese Gleichungen nach F aufzulösen<br />
Hierzu muß man zunächst überlegen, welcher Art die Quell- und Zielbereiche sind, die<br />
den Funktionen F und τ zugrundeliegen, und welche Funktionen F als Lösungen nur in<br />
Frage kommen können o<strong>der</strong> sollen. Sodann muß man den Bereichen, auf denen F und τ<br />
operieren, eine geeignete Struktur aufprägen und für τ nur noch gewisse strukturverträgliche<br />
Abbildungen zulassen, so daß für solche τ ein eindeutig bestimmter Fixpunkt existiert<br />
und ermittelt werden kann.<br />
0 Sei f: M→M eine Funktion. Jedes x∈M mit f(x)=x heißt Fixpunkt von f.<br />
Beispiel: Sei f: IN→IN definiert durch f(x)=Summe aller Teiler von x ohne x selbst. f besitzt Fixpunkte, z.B.<br />
f(6)=1+2+3=6,<br />
f(28)=1+2+4+7+14=28.
<strong>11</strong>-25<br />
Die grundlegenden Arbeiten zu diesem Problemkreis stammen von Dana S. Scott:<br />
Outline of a mathematical theory of computation, 1970. Er schlägt als grundlegende<br />
Struktur <strong>der</strong> Bereiche den vollständigen Verband vor. Später hat sich gezeigt, daß<br />
bereits die Struktur vollständiger partieller ordnungen (cpo) ausreicht.<br />
Welche Intuition steckt hinter diesen partiellen Ordnungen Die typischen Bereiche in<br />
<strong>der</strong> Funktionensemantik sind Datentypen wie int, bool usw. τ operiert auf Funktionenklassen<br />
über diesen Typen. Da die Funktionen auch partiell definiert sein können, empfiehlt<br />
es sich, die Datentypen zu vervollständigen, also um das "bottom"-Element ⊥ (Bedeutung:<br />
"undefiniert") zu erweitern, wie wir es bereits mehrfach getan haben. Partielle<br />
Funktionen lassen sich dann bezgl. ihres Grades an Definiertheit ordnen. Kleinstes<br />
Element bezgl. dieser Ordnung (hier mit ≤ bezeiechnet) ist die total undefinierte Funktion<br />
⊥ mit<br />
⊥(x)=⊥ für alle x.<br />
Zwei Funktionen f und g sind vergleichbar nach folgen<strong>der</strong> Vorschrift:<br />
f≤g : für alle x: (f(x)≠⊥ => f(x)=g(x)),<br />
d.h. g ist an mindestens sovielen Stellen definiert wie f und stimmt an den Stellen, an<br />
denen f definiert ist, mit f überein. Offenbar gilt dann:<br />
⊥≤f für alle f.<br />
≤ ist eine partielle Ordnung. For<strong>der</strong>t man nun für die partielle Ordnung eine gewisse<br />
Abschlußeigenschaft, nämlich daß jede Kette x 1 ≤x 2 ≤x 1 ≤... ein Supremum (eine kleinste<br />
obere Schranke) besitzt, und für die Funktionale τ die Monotonie<br />
f≤g => τ(f)≤τ(g)<br />
sowie weitere Eigenschaften, darunter die Stetigkeit, so kann man schließlich zeigen,<br />
daß ein Fixpunkt <strong>der</strong> Gleichung<br />
F=τ(F)<br />
existiert. Im allgemeinen gibt es jedoch mehrere Fixpunkte, darunter aber nur genau<br />
einen bezgl. <strong>der</strong> Ordnung ≤ kleinsten Fixpunkt F 0 . F 0 ist also <strong>der</strong> Fixpunkt, <strong>der</strong> für die<br />
wenigsten Argumente definiert ist. Dieses F 0 definiert man als die gesuchte Semantik<br />
des Programms. F 0 läßt sich relativ einfach als Grenzwert<br />
F 0 =lim n→∞ τ n (⊥)<br />
berechnen. Grundlage dieses Ergebnisses ist <strong>der</strong> Fixpunktsatz von Kleene, den wir im<br />
folgenden ohne Beweis angeben.<br />
Satz: (Fixpunktsatz von Kleene)<br />
Sei (M,≤) eine vollständige partielle Ordnung und τ: M→M eine stetige Funktion. Dann<br />
besitzt τ einen bezgl. <strong>der</strong> Ordnung ≤ kleinsten Fixpunkt F 0 , und es gilt
<strong>11</strong>-26<br />
F 0 =lim n→∞ τ n (⊥).<br />
Der Fixpunktsatz von Kleene liefert folgendes konstruktives Verfahren, um den Fixpunkt<br />
zu berechnen:<br />
1. Schritt: Setze x 0 :=⊥.<br />
2. Schritt: Wie<strong>der</strong>hole für n=0,1,2,...<br />
x n+1 ←τ(x n ).<br />
Gilt dann irgendwann x r =x r+1 , so ist x r =F 0 <strong>der</strong> gesuchte kleinste Fixpunkt. Tritt dieser Fall<br />
nicht ein, so terminiert die Berechnung nicht, jedoch approximiert <strong>der</strong> Zwischenwert x n<br />
den gesuchten kleinsten Fixpunkt beliebig genau. Häufig gelingt es dann, den Fixpunkt<br />
F 0 =lim n→∞ x n<br />
durch Grenzwertbetrachtungen zu gewinnen.<br />
Nun können wir auch die Fixpunktgleichung aus Abschnitt <strong>11</strong>.2.2 lösen, wenn wir uns<br />
vergewissert haben, daß <strong>der</strong> Fixpunktsatz von Kleene in dieser Situation anwendbar ist.<br />
Tatsächlich gilt – und dies müssen wir hier ohne Beweis hinnehmen –, daß, wann immer<br />
die Bestimmung <strong>der</strong> semantischen Funktion eines Programms aus µML auf eine Fixpunktgleichung<br />
führt, die beteiligten Objekte die Voraussetzungen des Satzes von<br />
Kleene erfüllen.<br />
Folglich ist die implizite Funktionalgleichung <strong>der</strong> Form (s. letztes Beispiel <strong>11</strong>.2.2)<br />
E[ if x=0 then c else f(x-1)]u<br />
=<br />
1, falls u(x)=0,<br />
E[ if x=0 then c else f(x-1)] {x=α-1, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}, sonst,<br />
die Ausgangspunkt aller unserer Überlegungen zur Semantik war, sinnvoll, und sie<br />
besitzt nach dem Satz von Kleene einen eindeutig bestimmten kleinsten Fixpunkt, <strong>der</strong><br />
die Bedeutung von<br />
E[ if x=0 then c else f(x-1)]<br />
und damit des gesamten Programms definiert. Diesen Fixpunkt wollen wir im folgenden<br />
berechnen.<br />
Beispiele:<br />
1) Zur Abkürzung setzen wir<br />
F:= E[ if x=0 then c else f(x-1)],<br />
G:= D[ val x=x-1],<br />
u:= {x=α, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}.<br />
Dann ist folgende Gleichung zu lösen:
<strong>11</strong>-27<br />
1, falls u(x)=0<br />
F(u)=<br />
F(G(u)), sonst.<br />
Setzt man<br />
1, falls u(x)=0<br />
τ(F)(u)=<br />
F(G(u)), sonst,<br />
so kann man den Fixpunktsatz von Kleene verwenden und rechnen:<br />
1, falls u(x)=0 1, falls α=0<br />
τ(⊥)(u)= =<br />
⊥(G(u)), sonst ⊥, sonst.<br />
1, falls u(x)=0 1, falls α=0 1, falls α=0<br />
τ 2 (⊥)(u)= = 1, falls G(u)(x)=0 = 1, falls α-1=0<br />
τ(⊥)(G(u)), sonst ⊥(G(G(u))), sonst ⊥, sonst.<br />
Hierbei ist<br />
G(u)=D[ val x=x-1]u=u+{x=E[x-1]u}=u+{x=E[x-1]u-1}=u+{x=α-1}<br />
={x=α-1, c=1, f=(x, if x=0 then c else f(x-1),{c=1})}<br />
und folglich<br />
G(u)(x)=α-1.<br />
Also gilt:<br />
1, falls α=0<br />
τ 2 (⊥)(u)= 1, falls α=1<br />
⊥, sonst.<br />
1, falls u(x)=0 1, falls α=0 1, falls α=0<br />
τ 3 (⊥)(u)= = 1, falls G(u)(x)=0 = 1, falls α-1=0<br />
τ 2 (⊥)(G(u)), sonst 1, falls G 2 (u)(x)=0 1, falls α-2=0<br />
⊥, sonst ⊥, sonst.<br />
Hierbei gilt – wie oben – G 2 (u)(x)=α-2. Also insgesamt<br />
1, falls α=0<br />
τ 3 (⊥)(u)= 1, falls α=1<br />
1, falls α=2<br />
⊥, sonst.<br />
Dann gilt offenbar allgemein:<br />
1, falls 0≤α≤n-1,<br />
τ n (⊥)(u)=
<strong>11</strong>-28<br />
⊥, sonst.<br />
Für den Grenzwert gilt dann<br />
1, falls α≥0,<br />
lim n→∞ τn (⊥)(u)= =: F 0 (α).<br />
⊥, sonst.<br />
F 0 ist <strong>der</strong> kleinste Fixpunkt des Funktionals τ und die Semantik des Programms. Man<br />
überzeuge sich von <strong>der</strong> Fixpunkteigenschaft durch Einsetzen von F 0 in die Gleichung<br />
τ(F)=F. Das Programm berechnet also die Funktion<br />
1, falls α≥0,<br />
F 0 (α)=<br />
⊥, sonst.<br />
2) Wir bestimmen die Semantik des Programms P"<br />
fun f x= if x=0 then 0 else f x;<br />
(read(y); f(y)).<br />
Für einen beliebigen Eingabewert α:int gilt:<br />
B[P"](α)=E[f(y)]((D[ fun f x= if x=0 then 0 else f x]∅)+{y=α})<br />
=E[f(y)](∅+{f=(x, if x=0 then 0 else f x,∅)}+{y=α})<br />
=E[f(y)]{y=α,f=(x, if x=0 then 0 else f x,∅)}.<br />
Setzen wir zur Abkürzung<br />
u={y=α,f=(x, if x=0 then 0 else f x,∅)}.<br />
Dann gilt:<br />
E[f(y)]u=E[ if x=0 then 0 else f x]<br />
(D[ fun f x= if x=0 then 0 else f x; val x=E[y]u]υ(u(f)))=<br />
E[ if x=0 then 0 else f x] (D[ fun f x= if x=0 then 0 else f x; val x=E[y]u]∅)=<br />
E[ if x=0 then 0 else f x]{x=α, f=(x, if x=0 then 0 else f x,∅)}.<br />
Mit u'={x=α, f=(x, if x=0 then 0 else f x,∅)} gilt dann:<br />
(*) E[ if x=0 then 0 else f x]u'<br />
E[0]u'=0, falls E[x=0]u'=wahr, d.h., falls u'(x)=α=0,<br />
=<br />
E[f x]u', sonst.<br />
Hierbei ist<br />
(**) E[f x]u'=E[ if x=0 then 0 else f x]<br />
(D[ fun f x= if x=0 then 0 else f x; val x=E[x]u']υ(u'(f)))=<br />
E[f x]u'=E[ if x=0 then 0 else f x]<br />
(D[ fun f x= if x=0 then 0 else f x; val x=E[x]u']∅)=<br />
E[ if x=0 then 0 else f x]u'.<br />
Nun betrachte man die Zeilen (*) und (**), die zu folgen<strong>der</strong> Gleichung führen:
<strong>11</strong>-29<br />
0, falls u'(x)=0,<br />
(***) E[ if x=0 then 0 else f x]u'=<br />
E[ if x=0 then 0 else f x]u', sonst.<br />
Setzt man nun zur Abkürzung<br />
F:= E[ if x=0 then 0 else f x],<br />
so erhält man aus (***) folgende Funktionalgleichung<br />
0, falls u'(x)=0,<br />
F(u')=<br />
F(u'), sonst.<br />
Wir wenden den Satz von Kleene an und berechnen den Fixpunkt, indem wir setzen<br />
0, falls u'(x)=0<br />
τ(F)(u')=<br />
F(u'), sonst,<br />
so kann man den Fixpunktsatz von Kleene verwenden und rechnen:<br />
0, falls u'(x)=0 0, falls α=0<br />
τ(⊥)(u)= =<br />
⊥(u'), sonst ⊥, sonst.<br />
0, falls u'(x)=0 0, falls α=0 0, falls α=0<br />
τ 2 (⊥)(u)= = 0, falls u'(x)=α=0 =<br />
τ(⊥)(u'), sonst ⊥(u'), sonst ⊥, sonst.<br />
Also gilt:<br />
0, falls α=0<br />
τ 2 (⊥)(u)=<br />
⊥, sonst.<br />
Wegen τ 2 (⊥)(u)=τ(⊥)(u) ist F 0 :=τ(⊥) <strong>der</strong> kleinste Fixpunkt und die Semantik des Programms<br />
P". Man überzeuge sich von <strong>der</strong> Fixpunkteigenschaft durch Einsetzen von<br />
F 0 in die Gleichung τ(F)=F. Das Programm berechnet also die Funktion<br />
0, falls α=0,<br />
F 0 (α)=<br />
⊥, sonst.