20.01.2015 Aufrufe

Kapitel 11 - Grundlagen der Programmiersprachen - DdI

Kapitel 11 - Grundlagen der Programmiersprachen - DdI

Kapitel 11 - Grundlagen der Programmiersprachen - DdI

MEHR ANZEIGEN
WENIGER ANZEIGEN

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.

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

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!