Kapitel 11 - Grundlagen der Programmiersprachen - A. Schwill - 1997

Kapitel 11 - Grundlagen der Programmiersprachen - A. Schwill - 1997

11-1

11 Grundlagen der Programmiersprachen

In diesem Abschnitt verfolgen wir das Ziel, Programmiersprachen exakt zu beschreiben.

Die beiden wichtigsten Aspekte, die eine Programmiersprache kennzeichnen, sind Syntax und Semantik.

Die Syntax legt fest, welche Aneinanderreihungen von Zeichen korrekte Sätze der Sprache sind. Die Semantik bestimmt die inhaltliche Bedeutung aller Sätze der Sprache.

Syntax und Semantik bilden also ein untrennbares Paar, wenn es um die Beschreibung von Sprachen, und speziell von Programmiersprachen geht. Folglich definiert man eine

Programmiersprache P als ein Paar P=(L, F ), wobei L

A

*

eine formale Sprache über dem Alphabet A={a,...,z,A,...Z,0,...,9,:,;,-,...} ist und die Syntax von P bestimmt und F eine

Abbildung, die jedem Wort (Programm) w

L seine Bedeutung (Semantik) zuordnet.

Für die exakte Beschreibung von Syntax und Semantik von Programmiersprachen gibt es eine Reihe von Methoden, von denen wir die wichtigsten im folgenden vorstellen.

11.1 Syntax

In Kapitel 2 hatten wir bereits gesehen, daß Programmiersprachen stets eine feste, präzise Syntax besitzen müssen. In dieser Kurseinheit definieren wir zunächst den Begriff der Sprache und führen anschließend einige Methoden ein, mit denen man die Syntax von Sprachen exakt festlegen kann.

11.1.1 Alphabet und Sprache

(Natürliche) Sprachen (Deutsch, Englisch, Französisch, Japanisch, Kisuaheli usw.) werden von Menschen zum Informationsaustausch und zu allen Zwecken der Kommunikation verwendet. Die Beherrschung einer Sprache und ihrer Ausdrucksmöglichkeiten beeinflußt stark die Vorstellungswelt und die Denkweise von Menschen. Zu jeder natürlichen Sprache existiert in der Regel eine Schriftsprache; manche Sprachen (z.B. Hethitisch) liegen nur noch in dieser Form vor.

In der Informatik interessiert man sich für künstliche Sprachen, unter denen die Programmiersprachen die wichtigsten sind. Künstliche Sprachen wurden Ende des 19. Jahrhunderts entwickelt, um Fakten, Denkabläufe, Schlußfolgerungen beschreiben und analysieren zu können. Neben diesen meist in der Mathematik verwendeten logischen

Kalkülen entstanden mit der Entwicklung von Computern seit 1940 Programmiersprachen.

11-2

Schriftsprachen (im Gegensatz zu Bildsprachen, z.B. Chinesisch), zu denen auch die

Programmiersprachen gehören, sind über einem Alphabet, d.h. einer (endlichen) Menge von unterscheidbaren Zeichen (Buchstaben) aufgebaut. Die Buchstaben sind geordnet, d.h., es liegt fest, welches der erste, der zweite, der dritte usw. Buchstabe des Alphabets ist.

Durch Aneinanderreihung endlich vieler Buchstaben eines Alphabets kann man Wörter

(Zeichenfolgen) bilden. Eine Sprache über einem Alphabet ist dann eine Teilmenge der

Menge aller Wörter, die man mit den Buchstaben des Alphabets bilden kann.

Diese sehr unpräzise Erläuterung der Begriffe wollen wir nun formalisieren.

Definition A:

Eine (zweistellige) Relation R auf einer Menge M ist eine Teilmenge R

M

×

M. Sind a,b

M und (a,b)

R, so sagt man "a und b stehen in der Relation R". Gelegentlich schreibt man statt (a,b)

R auch aRb. Eine Relation heißt

- reflexiv, wenn für alle a

M stets (a,a)

R gilt;

- transitiv, wenn für alle a,b,c

M aus (a,b)

R und (b,c)

R stets (a,c)

R folgt;

- symmetrisch, wenn für alle a,b

M aus (a,b)

R auch (b,a)

R folgt;

- antisymmetrisch, wenn für alle a,b

M aus (a,b)

R und (b,a)

R stets a=b folgt.

Beispiele:

1) Man betrachte eine Verwandschaftsrelation. M sei die Menge aller Menschen.

R

M

×

M sei die Menge aller Paare von Menschen, die miteinander verwandt sind, d.h. (a,b)

R, wenn die Personen a und b miteinander verwandt sind.

R ist nicht reflexiv, denn eine Person ist nicht mit sich selbst verwandt (Man kann das aber auch anders sehen). R ist symmetrisch, denn wenn von zwei Menschen a,b die

Person a mit b verwandt ist ((a,b)

R), dann ist auch b mit a verwandt ((b,a)

R). R ist nicht antisymmetrisch.

Ist R transitiv? Wenn von drei Personen a,b,c die Person a mit b und b mit c verwandt ist ((a,b)

R und (b,c)

R), dann ist meist auch a mit c verwandt ((a,c)

R). Leider gilt dies aber nicht immer; denn wenn z.B. Ehepartner jeweils ein Kind a und c, die nicht verwandt sind ((a,c)

R), in eine Ehe einbringen und dann ein gemeinsames Kind b haben, dann ist b sowohl mit a als auch mit c verwandt (Halbgeschwister), also folgt aus (a,b)

R und (b,c)

R im allgemeinen nicht (a,c)

R.

2) Man betrachte die Kleiner-Gleich-Relation. M sei gleich der Menge der natürlichen

Zahlen IN. R

IN

×

IN sei die Menge aller Paare natürlicher Zahlen (a,b), für die a

≤ b gilt. R ist reflexiv, transitiv und antisymmetrisch.

11-3

Definition B: a) Eine reflexive, transitive und symmetrische Relation R

M

×

M auf einer Menge M heißt

Äquivalenzrelation.

b) Eine reflexive, transitive und antisymmetrische Relation heißt (partielle) Ordnung oder Halbordnung.

c) Ist die Ordnung R total definiert, d.h., gilt für alle a,b

M die Beziehung (a,b)

R oder

(b,a)

R, so ist R eine lineare oder totale Ordnung.

Bezeichnung: Ist R eine Ordnung, so schreibt man anstelle von (a,b)

R meist a

≤ b. Ist R antisymmetrisch und transitiv, so schreibt man meist a<b.

Anschaulich gesprochen sind in einer linearen Ordnung alle, in einer partiellen Ordnung

manche Elemente paarweise miteinander vergleichbar.

Beispiele:

1) Man betrachte die Relation "ist gleich oder ist Chef von" innerhalb eines Betriebes. M ist also die Menge der Mitarbeiter. R

M

×

M ist die Menge aller Paare von Mitarbeitern

(a,b), wobei a=b oder a Chef von b ist, also a

≤ b gilt. R ist reflexiv, transitiv und antisymmetrisch, folglich eine (partielle) Ordnung. R ist aber keine lineare Ordnung, denn für zwei Mitarbeiter a und b aus unterschiedlichen Abteilungen gilt z.B. weder a

≤ b, also a ist Chef von b, noch b

≤ a, also b ist Chef von a.

2) Auf der Menge IN der natürlichen Zahlen ist die Kleiner-Gleich-Relation R (s.o.

Beispiel 2) eine Ordnung. R ist außerdem eine lineare Ordnung, denn für zwei beliebige Elemente a,b

IN gilt a

≤ b oder b

≤ a.

Nach diesen Vorbereitungen können wir die Begriffe "Alphabet" und "Wort" definieren, bzw. wiederholen.

Definition C:

Eine nichtleere endliche Menge A={a

1

,a

2

,...,a n

}, n

1, mit einer linearen Ordnung a

1

<a

2

, a

2

<a

3

,..., a n-1

<a n

heißt Alphabet.

Zur Erinnerung: Sei A={a

1

,a

2

,...,a n

} ein Alphabet. Ein Wort w der Länge k über A ist eine

Folge von k Elementen von A, d.h.

w=b

1 b

2

...b

k

mit b j

A für j=1,...,n und n

IN.

11-4

Die Länge von w bezeichnet man durch |w|=k. Das Wort der Länge 0 heißt leeres

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 ist das Wort, das sich durch Konkatenation (Aneinanderfügen) aus v und w ergibt, d.h.

vw=b

1 b

2

...b

i c

1 c

2

...c

j

.

Diese Operation ist assoziativ und besitzt mit dem leeren Wort ein Einselement. Die

Menge aller Wörter über A, das sogenannte freie Monoid über A, wird mit A

*

bezeichnet und ist definiert durch:

A

*

={b

1 b

2

...b

k

| b i

A für i=1,...,k und k

IN}.

Beispiele:

1) Sei A={a,b} mit a<b ein Alphabet. w=aabbab ist ein Wort über A mit |w|=6. Sei v=babbb. Dann ist die Konkatenation von v und w das Wort vw=babbbaabbab. A

*

ist die Menge der Wörter

{

ε

,a,b,aa,ab,ba,bb,aaa,aab,aba,abb,baa,... }

2) Sei A={ | }. Dann ist

A

*

={

ε

, |, ||, |||, ||||, |||||,...} ("Bierdeckelnotation").

A

*

ist isomorph zur Menge IN, wenn man

ε

der Null und n Striche der Zahl n

IN zuordnet. Die Konkatenation zweier Wörter v,w

A

*

entspricht dann der Addition, denn z.B. gilt für v=||| (entspricht der Zahl 3) und w=||||| (entspricht der Zahl 5) vw=|||||||| (entspricht der Zahl 8).

Definition D:

Sei A ein Alphabet. Jede Teilmenge L

A

*

heißt Sprache über A.

Beispiele:

1) Sei A={0,1,2,...,9} das Alphabet der üblichen Dezimalziffern. Die Menge

L={a

1 a

2

...a

n

| a i

A für i=1,...,n; n

IN, n

1; a

1

0 falls n

2} ist eine Sprache über A, die Sprache der Dezimaldarstellungen für die natürlichen

Zahlen (ohne führende Nullen).

2) Sei A={ | } ein Alphabet. Die Menge

L={a

1 a

2

...a

n

| a i

A für i=1,...,n; n Primzahl} ist eine Sprache über A. L ist die Menge aller Strichfolgen, deren Länge eine

Primzahl ist.

Wir haben nun die für die folgenden Untersuchungen zentralen Begriffe "Alphabet",

"Wort" und "Sprache" kennengelernt. Allerdings war besonders die Definition von Sprache noch recht abstrakt und unhandlich. Im nächsten Abschnitt widmen wir uns daher

11-5 der Frage, wie man Sprachen möglichst anschaulich erfassen und beschreiben kann.

Zuvor wollen wir jedoch zur Erleichterung unserer Notation noch einige neue Bezeichnungen einführen.

Bezeichnungen:

1) Die Menge aller Wörter der Länge n über einem Alphabet A bezeichnet man mit A n

:

A n

={w | w

A

*

und |w|=n}.

Speziell gilt für n=0:

A

0

={

ε

}.

2) Entfernt man aus A

*

das leere Wort, so erhält man die Menge

A

+

=A

*

\{

ε

}.

3) Sei w

A

*

und n

IN. w n

bezeichnet die n-fache Aneinanderreihung von w, d.h.

w n

=www...ww.

Speziell gilt für n=0 wiederum: w

0

=

ε

.

11.1.2 Syntaxdiagramme

Unter der Syntax einer Sprache verstehen wir die Regeln, denen die Wörter, die zur

Sprache gehören, gehorchen müssen. Wir beschäftigen uns in diesem Abschnitt mit einer anschaulichen und sehr einfachen Methode, um die Syntax von Sprachen zu beschreiben, den sog. Syntaxdiagrammen.

Definition E:

Seien N und T Alphabete. N ist die Menge der Nichtterminalsymbole (auch Hilfszeichen oder Variablen genannt) und T die Menge der Terminalsymbole. Syntaxdiagramme

über N und T sind folgendermaßen aufgebaut:

1) Jedes Syntaxdiagramm ist mit einem Nichtterminalsymbol aus N markiert.

2) Ein Syntaxdiagramm besteht aus Kreisen (oder Ellipsen) und Kästchen, die durch

Pfeile miteinander verbunden sind.

3) In jedem Kreis (oder jeder Ellipse) steht ein Wort über T. In jedem Kästchen steht ein

Nichtterminalsymbol, d.h., ein Element aus N.

4) Aus jedem Kreis (jeder Ellipse) und jedem Kästchen führt genau ein Pfeil hinaus und genau ein Pfeil hinein.

5) Ein Pfeil darf sich in mehrere Pfeile aufspalten (Verzweigungspunkt) und mehrere

Pfeile dürfen zu einem Pfeil zusammengeführt werden (Zusammenfassungspunkt).

6) In jedem Syntaxdiagramm gibt es genau einen ("Eingangs"-) Pfeil, der von keinem

Kreis (Ellipse) oder Kästchen ausgeht (von außen in das Syntaxdiagramm einlaufender Pfeil), und genau einen ("Ausgangs"-) Pfeil, der zu keinem Kreis (Ellipse) oder

11-6

Kästchen führt (nach außen aus dem Syntaxdiagramm auslaufender Pfeil). Alle

übrigen Pfeile verbinden Kästchen, Kreise (Ellipsen), Verzweigungs- und Zusammenführungspunkte.

7) Von dem Eingangspfeil aus kann man jeden Kreis (Ellipse) und jedes Kästchen des

Syntaxdiagramms auf mindestens einem Weg erreichen, und von jedem Kreis

(Ellipse) oder Kästchen kann man auf mindestens einem Weg zum Ausgangspfeil gelangen. (Diese Bedingung besagt, daß das Syntaxdiagramm nicht in mehrere

Teile zerfallen darf.)

Soweit die Definition von Syntaxdiagrammen.

Beispiel: Sei T={0,1,2,...,9,+,-} das Alphabet der Terminalsymbole, N={

Ziffer

,

Ziffernfolge

,

ZifferohneNull

,

Ganzzahl

} die Menge der Nichtterminalsymbole. Syntaxdiagramme über N und T zeigen Abb. 1.

11-7

ZifferohneNull

1 2 3 4 5 6 7 8 9

Ziffer

0 ZifferohneNull

Ziffernfolge

Ziffer

Ganzzahl

+

0

ZifferohneNull Ziffernfolge

Abb.1: Beispiele für Syntaxdiagramme

Syntaxdiagramme über N und T definieren eindeutig eine Sprache L

T

*

. Wie bestimmt man aber nun diese Sprache?

Definition F: Auswertung eines Syntaxdiagramms

11-8

Man betritt das Syntaxdiagramm durch den Eingangspfeil und befolgt solange jeweils eine der folgenden Regeln, bis man das Syntaxdiagramm auf dem Ausgangspfeil verlassen hat:

1) Trifft man auf einen Verzweigungspunkt, so folgt man nach Belieben einem der weiterführenden Pfeile.

2) Trifft man auf einen Zusammenfassungspunkt, so folgt man dem weiterführenden

Pfeil.

3) Trifft man auf einen Kreis (eine Ellipse), so schreibt man seinen Inhalt, also ein Wort

über T, auf (bzw. hängt es hinten an das schon aufgeschriebene Wort an) und verläßt den Kreis (die Ellipse) über den ausgehenden Pfeil.

4) Trifft man auf ein Kästchen, in dem ein Nichtterminalsymbol

ω

steht, so kopiert man das Syntaxdiagramm mit der Bezeichung

ω

anstelle des Kästchen ein. Der in das

Kästchen hineinlaufende Pfeil wird mit dem Eingangspfeil des Syntaxdiagramms für

ω

und der von dem Kästchen abgehende Pfeil mit dem Ausgangspfeil des Syntaxdiagramms für

ω

verbunden.

Die Regel 4 gilt für jedes mögliche Nichtterminalsymbol

ω

. Insbesondere kann ein Syntaxdiagramm auch in sich selbst eingesetzt werden. Dadurch ist ein rekursives Einsetzen möglich!

Nun wird auch klar, wie die Namen "Terminalsymbol" und "Nichtterminalsymbol" zustande kommen: Während Terminalsymbole einen Endzustand (lat.: terminare = beenden) darstellen, symbolisieren Nichtterminalsymbole einen Zwischenzustand und können weiter ersetzt werden.

Definition G:

Jedes Wort, das nach irgendeiner Auswertung eines Syntaxdiagramms aufgeschrieben ist, kann durch das Syntaxdiagramm erzeugt werden. Die durch das Syntaxdiagramm

definierte Sprache ist die Menge aller Wörter, die auf diese Weise erzeugt werden kann.

Beispiele:

1) Wir betrachten noch einmal die Syntaxdiagramme aus Abb. 1.

Zunächst betrachten wir das Syntaxdiagramm

ZifferohneNull

. Wir betreten es links durch den Eingangspfeil, gehen bei den Verzweigungspunkten z.B. zweimal geradeaus und dann nach unten. Dort treffen wir auf den Kreis mit der Inschrift 3. Wir schreiben 3 auf und verlassen auf den unten stehenden Pfeilen das Syntaxdiagramm. Die Zeichenfolge "3" ist also ein Wort der vom Syntaxdiagramm

ZifferohneNull

11-9 definierten Sprache. Man sieht unmittelbar, daß die Sprache, die dieses Syntaxdiagramm definiert, genau folgende endliche Menge ist:

{1,2,3,4,5,6,7,8,9}.

Betritt man das Syntaxdiagramm

Ziffer

, so kann man entweder das Zeichen 0 hinschreiben, oder man wählt das Kästchen

ZifferohneNull

. Dieses Kästchen ist nun durch das Syntaxdiagramm

ZifferohneNull

zu ersetzen. Wie man dieses auswertet, haben wir bereits diskutiert. Die von

Ziffer

definierte Sprache lautet daher:

{0,1,2,3,4,5,6,7,8,9}.

Das Syntaxdiagramm

Ziffernfolge

kann man, ohne auf einen Kreis oder ein Kästchen zu treffen, wieder verlassen, d.h., das leere Wort gehört zu der hierdurch definierten

Sprache. Man kann sich aber beim Verzweigungspunkt für die untere Richtung entscheiden und eine Ziffer erzeugen; dies kann man beliebig of t wiederholen. Folglich definiert das Syntaxdiagramm

Ziffernfolge

genau die Menge aller Wörter über den

Ziffern, also

{0,1,2,3,4,5,6,7,8,9}

*

.

Man kann sich davon überzeugen, daß die durch

Ganzzahl

definierte Sprache die

Menge aller Dezimaldarstellungen der ganzen Zahlen ist (ohne führende Nullen, ggf.

mit Vorzeichen).

2) Im Laufe der Vorlesung haben wir regelmäßig den Begriff des Bezeichners verwendet, ohne jemals genau zu definieren, was wir unter einem Bezeichner verstehen. Mit

Hilfe von Syntaxdiagrammen können wir diese Lücke nun schließen (Abb. 2). Ein

Bezeichner ist nach dieser Definition also eine Folge von Buchstaben und/oder Ziffern, die stets mit einem Buchstaben beginnt.

11-10

Ziffer

0 1 2 3 4 5 6 7 8 9

Buchstabe a A b B

...

z Z

Bezeichner

Buchstabe

Buchstabe

Ziffer

Abb. 2: Syntaxdiagramme für Bezeichner

Wir haben Syntaxdiagramme eingeführt, um Sprachen präzise definieren können. Dabei haben wir aber noch nicht überprüft, ob Syntaxdiagramme ein genügend mächtiges

Darstellungsmittel in dem Sinne sind, daß man jede beliebige Sprache L

T

*

über einem

Alphabet T mit ihnen beschreiben kann. Wir wollen hier ein Ergebnis vorwegnehmen, welches in Vorlesungen über Formale Sprachen erarbeitet wird. Man kann beweisen, daß mit Syntaxdiagrammen aus theoretischer Sicht zwar nur relativ eingeschränkte, aber gerade die für die Praxis sehr interessanten Sprachen beschrieben werden können, die sog. kontextfreien Sprachen. Die Syntax sehr vieler bekannter Programmiersprachen (auch die von uns benutzten Programmiersprachen PRO und ASS) fällt im wesentlichen (Präzisierung: siehe Vorlesungen über Theoretische Informatik) unter die

11-11 kontextfreien Sprachen. Daß es aber auch sehr einfache Sprachen gibt, die nicht durch

Syntaxdiagramme definiert werden können, zeigt folgendes

Beispiel: T sei die Menge { | }. Man kann zeigen, daß die Sprache L

T

*

, die genau alle

Folgen mit quadratisch vielen Strichen enthält, also

L={ | n

| n ist Quadratzahl}, nicht durch ein Syntaxdiagramm definiert werden kann.

11.1.3 Backus-Naur-Form (BNF)

Eine andere nicht-graphische Methode zur Darstellung von Sprachen ist die Backus-

Naur-Form, die von den beiden Wissenschaftlern J. Backus und P. Naur vor über 30

Jahren vorgeschlagen und von einem Komitee zur Definition der Programmiersprache

ALGOL 60 verwendet wurde.

Dieser Ansatz verläuft, vereinfacht gesprochen, so: Man beginnt mit einer Zeichenfolge und erzeugt durch eine Folge von Veränderungsschritten potentiell jedes mögliche Wort der Sprache. Die Regeln, nach denen Veränderungen an Zeichenfolgen vorgenommen werden dürfen, bezeichnet man als Grammatik. Es ist klar, daß wir zur Notation des

Regelwerks – wie schon bei Syntaxdiagrammen – spezielle Symbole benötigen, die mit den Zeichen der Sprache, die wir definieren wollen, nicht verwechselt werden können.

Fachmännisch gesprochen: Wir benötigen eine Metasprache, d.h. eine Sprache, mit der wir eine andere Sprache mit Hilfe einer Grammatik definieren können. Eine solche

Metasprache ist die Backus-Naur-Form.

Wie schon bei Syntaxdiagrammen unterscheidet man bei einer Grammatik in Backus-

Naur-Form zwei Zeichenmengen: ein Alphabet N von Nichtterminalsymbolen und ein

Alphabet T von Zeichen der zu definierenden Sprache, die Terminalsymbole.

Jedes Nichtterminalsymbol ist in spitze Klammern <> eingeschlossen, die nicht als Terminalsymbole vorkommen dürfen. Dies hat den Zweck, Nichtterminalsymbole kenntlich zu machen, denn sie sind ja meist mit Hilfe des gleichen Alphabets geschrieben wie die

Sprache selbst (nämlich im Alphabet der deutschen Sprache). In der Menge der Nichtterminalsymbole zeichnet man ein Symbol S besonders aus, das Startsymbol.

Das Startsymbol ist gewissermaßen der allgemeinste Begriff, d.h., die Zeichenfolge, bei der man mit der Durchführung von Veränderungsschritten beginnt. Die Veränderungsregeln selbst bezeichnet man als Regeln oder Produktionen. Sie werden in der Backus-

Naur-Form folgendermaßen aufgeschrieben:

X ::= w

1

| w

2

| ... | w n

.

11-12

Dabei ist X

N stets ein einzelnes (in spitze Klammern gefaßtes) Nichtterminalsymbol.

w

1

,w

2

,...,w n

(N

T)

*

sind n beliebige Wörter, die aus Nichtterminal- und Terminalsymbolen gebildet sind (auch das leere Wort

ε

ist möglich). Die obige Produktion bedeutet:

Wenn in einem Wort an irgendeiner Stelle das Nichtterminalsymbol

X vorkommt, so darf es durch eines der Worte w

1

,w

2

, ...,w n

ersetzt werden.

Damit dieser Ersetzungsschritt problemlos durchgeführt werden kann, dürfen die Symbole "::=" und "|" nicht als Terminalsymbole vorkommen. Sie gehören zur Metasprache.

Das Viertupel G=(N,T,P,S), bestehend aus der Menge der Nichtterminalsymbole N, der

Menge der Terminalsymbole T, der Menge der Produktionen P und dem Startsymbol S bezeichnet man als Grammatik in Backus-Naur-Form (BNF-Grammatik).

Beispiel: Die folgende BNF-Grammatik entspricht den Syntaxdiagrammen in Abb. 1. Es sei

T={0,1,2,...,9,+,-},

N={

<Ziffer>,<ZifferohneNull>,<Ziffernfolge>,<Ganzzahl>

}.

Das Startsymbol S sei das Symbol

<Ganzzahl>

. Die Produktionen seien definiert durch:

<

ZifferohneNull

>::= 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

<

Ziffer

>::= 0 | <

ZifferohneNull

>

< Ziffernfolge >::=

ε

| < Ziffer >< Ziffernfolge >

<

Ganzzahl

>::= 0 | +0 | -0 | <

ZifferohneNull

><

Ziffernfolge

> |

+ <

ZifferohneNull

><

Ziffernfolge

> |

- <

ZifferohneNull

><

Ziffernfolge

>

Definition H:

Eine Grammatik in Backus-Naur-Form, kurz BNF-Grammatik, ist ein Viertupel

G=(N,T,P,S) mit a) T ist die Menge der Terminalsymbole, N die Menge der Nichterminalsymbole und T

N=

; b) S

N ist das Startsymbol; c) P

P ist eine Zeichenfolge der

Form wobei X

X ::= w

N, n

1

| w

2

| ... | w n

.

1 und w i

(N

T)

*

für i=1,2,...,n sind.

Wie kann man nun mit Hilfe einer BNF-Grammatik aus dem Startsymbol Wörter erzeugen, und was ist die von der Grammatik definierte Sprache? Wie bereits kurz angeschnitten, beginnt man mit dem Startsymbol S. Wann immer ein Wort über T

N, also

11-13 bestehend aus Terminal- und Nichtterminalsymbolen, vorliegt, in dem das Nichtterminalsymbol X vorkommt, darf man X durch eines der Wörter w

1

,w

2

,...,w n ersetzen, sofern

X::=w

1

| w

2

| ... | w n

eine Produktion der Grammatik ist. Die Ersetzungsschritte brechen ab, wenn man ein Wort über T erhalten hat, welches also nur noch aus Terminalsymbolen besteht.

Definition I:

Sei G=(N,T,P,S) eine BNF-Grammatik.

a) Ein Wort v

(N

T)

*

heißt in einem Schritt aus einem Wort u

(N

T)

*

ableitbar, wenn es Wörter u

X::=w

1

|w

2

|...|w

1

,u

2

(N

T)

*

, ein X

N und eine Produktion der Form n

in P gibt, so daß für ein i

{1,...,n} gilt: u=u

1

Xu

2

und v=u

1 wu

2

.

Man schreibt dann kurz u

→ v.

b) Ein Wort v

(N

T)

*

heißt ableitbar aus einem Wort u

(N

T)

*

, wenn u=v ist oder wenn es Wörter u=u

0

,u

1

,u

2

,...,u r

=v

(N

T)

*

gibt, so daß u i-1

→ u i

für i

{1,..., r} gilt. Man schreibt dann kurz u

* v.

u

0

→ u

1

→ u

2

...

→ u r

heißt Ableitung der Länge r von u

0

nach u r

.

c) Die von einer BNF-Grammatik G erzeugte Sprache ist definiert durch

L(G)={w

T

*

| S

* w}.

L(G) ist also die Menge aller aus dem Startsymbol ableitbaren Wörter, die nur aus

Terminalsymbolen bestehen.

Beispiele:

1) Man betrachte obiges Beispiel. Frage: Ist das Wort 732 aus dem Startsymbol ableitbar? Ja, denn man kann folgende Ableitung bilden:

< Ganzzahl >

< ZifferohneNull > < Ziffernfolge >

< ZifferohneNull > < Ziffer > < Ziffernfolge >

7 < Ziffer >< Ziffernfolge >

7 < Ziffer > < Ziffer > < Ziffernfolge >

7 < ZifferohneNull > < Ziffer > < Ziffernfolge >

7 < ZifferohneNull > < Ziffer >

73 < Ziffer >

73 < ZifferohneNull >

732.

Die von der BNF-Grammatik G erzeugte Sprache L(G) ist wie beim zugehörigen

Syntaxdiagramm die Menge der Dezimaldarstellungen der ganzen Zahlen ohne führende Nullen und ggf. mit Vorzeichen.

2) Sei G=(N,T,P,<start>) eine BNF-Grammatik mit N={

<start>

}, T={a,b},

11-14

P={ <start> ::=

ε

| a <start> | b <start> }.

Mit G kann man jede beliebige Folge von a und b erzeugen, d.h. L(G)=T

*

.

3) Sei G=(N,T,P,S) eine BNF-Grammatik mit N={

<start>

}, T={(,)},

P={ <start> ::=( <start> ) <start> |

ε

}.

Mit G kann man alle korrekten Klammerungen erzeugen, d.h., alle Klammerfolgen, bei denen an jeder Stelle der Folge mehr Klammern geöffnet als geschlossen wurden und zusätzlich am Ende der Folge die Zahl der geöffneten Klammern gleich der

Zahl der geschlossenen Klammern ist. Zum Beispiel ist ((()())()) eine korrekte,

((())()))())) eine unkorrekte Klammerung.

Eine Ableitung soll die Wirkungsweise verdeutlichen:

<start>

( <start> ) <start>

(( <start> ) <start> ) <start>

(( <start> ) <start> )( <start> ) <start>

(() <start> )( <start> ) <start>

(())( <start> ) <start>

(())() <start>

(())()

Dabei ist in jedem zwischenzeitlich abgeleiteten Wort das Nichtterminalsymbol unterstrichen, das im nächsten Schritt ersetzt wird.

4) Die folgende Grammatik G=(N,T,P,<

Bezeichner

>) mit

N={<

Bezeichner

>,<

Buchstabe

>,<

Zeichenkette

>,<

Ziffer

>},

T={a,b,...,z,0,1,...,9} und

P={<

Bezeichner

>::= <

Buchstabe

> | <

Buchstabe

><

Zeichenkette

>,

<

Zeichenkette

>::= <

Buchstabe

> | <

Ziffer

> |

<

Buchstabe

><

Zeichenkette

> |

<

Buchstabe

>::= a | b | ... | z} definiert die Sprache der Wörter über T, die als Bezeichner verwendet werden dürfen.

<

Ziffer

><

Zeichenkette

>,

<

Ziffer

>::= 0 | 1 | ... | 9,

BNF-Grammatiken dienen in der Informatik wie Syntaxdiagramme meist dazu, die Syntax von Programmiersprachen zu definieren.

Für bestimmte Produktionen hat man in der Backus-Naur-Form Abkürzungen eingeführt.

1. Abkürzung: Symbole oder Symbolfolgen innerhalb einer Produktion, die auch weggelassen werden können, werden in eckige Klammern eingeschlossen.

Beispiel: Eine Produktion der Form

A

::=

B C

|

B wobei B und C beliebige Symbolfolgen sind, kann man abkürzen durch

A

::=

B

[

C

].

11-15

Die Produktion für <

Bezeichner

> in obigem Beispiel 4 schreibt man dann entsprechend als

<

Bezeichner

> ::= <

Buchstabe

> [<

Zeichenkette

>]

2. Abkürzung: Symbole oder Symbolfolgen, die beliebig oft wiederholt oder aber auch weggelassen werden können, werden in geschweifte Klammern eingeschlossen.

Beispiel: Anstelle der Produktionen für <

Bezeichner

> und <

Zeichenkette

> in Beispiel 4 kann man nun kurz schreiben:

<

Bezeichner

>::= <

Buchstabe

> {<

Buchstabe

> | <

Ziffer

>}

Welche Mächtigkeit besitzen nun BNF-Grammatiken im Vergleich zu Syntaxdiagrammen? Man kann zeigen, daß man zu jeder BNF-Grammatik ein gleichwertiges Syntaxdiagramm konstruieren kann und umgekehrt. Den Beweis werden wir hier nicht führen; er ist relativ einfach, und man kann ihn sich an Beispielen leicht klar machen. Man kann also mit BNF-Grammatiken die gleichen Sprachen definieren wie mit Syntaxdiagrammen. Diese Sprachen nennt man die kontextfreien Sprachen.

Anhand der Backus-Naur-Form von Grammatiken läßt sich gut nachvollziehen, was der

Begriff "kontextfrei" bedeutet. Er bedeutet, daß innerhalb eines Ableitungsprozesses jedes Nichtterminalzeichen <

X

> ersetzt werden darf, unabhängig davon, in welchem

Kontext es auftritt, d.h., welche Zeichen rechts oder links neben <

X

> stehen.

Da alle bekannten Programmiersprachen im wesentlichen zu den kontextfreien Sprachen gehören, haben Syntaxdiagramme wie auch BNF-Grammatiken eine große Bedeutung. Doch Vorsicht! Wir haben gesagt "im wesentlichen" und meinen damit, daß gewisse, aber wenige syntaktische Regeln einer Programmiersprache nicht mit Hilfe eines Syntaxdiagramms oder einer BNF-Grammatik definiert werden können. Zwei dieser Regeln sind z.B.

- Jeder Bezeichner, der in einem Programm verwendet wird, muß vorher deklariert werden.

- Die Anzahl der in der Deklaration einer Prozedur auftretenden formalen Parameter ist gleich der Zahl der bei der Anwendung der Prozedur aufgeführten aktuellen Parameter.

Man kann zeigen, daß keine dieser beiden Regeln durch ein Syntaxdiagramm oder eine

BNF-Grammatik erzwungen werden kann. Trotz dieses Mangels definiert man Programmiersprachen stets durch Syntaxdiagramme oder BNF-Grammatiken. Regeln der obigen

Form, die man hiermit nicht beschreiben kann, fügt man umgangssprachlich hinzu.

11-16

11.2 Semantik

Wie zu Beginn dieses Kapitels erwähnt ist eine Programmiersprache P ein Paar P=(L, F ) mit der formalen Sprache L

A

*

, die die Syntax von P bestimmt, und der Abbildung F , die jedem Programm w

L seine Semantik zuordnet. Mit der Syntax haben wir uns soeben beschäftigt. Wie kann man nun die Semantik eines Programms präzisieren?

Schon früher (Abschnitt 4.5.1) hatten wir einem Programm mit der Methode der denotationalen Semantik die von w berechnete Funktion f w

: I

O von der Menge I der Eingabedaten in die Menge O der Ausgabedaten als Bedeutung zuordnet. F ist nach diesem Ansatz also eine Funktion

F : A

*

(I

O) mit f w

, falls w

L

F [w]=

, sonst.

F nennt man auch semantische Funktion. F liefert also zu jedem syntaktisch korrekten Programm als seine Bedeutung die zugehörige berechnete Funktion. Programme, die syntaktisch fehlerhaft sind, haben keine Bedeutung, und die semantische Funktion ist hierfür undefiniert.

Beispiel: Gegeben sei folgende Zeichenfolge w mit w

f x:int

int

≡ dann .

Offenbar ist w ein syntaktisch korrektes Programm. Wie lautet die Semantik von w? Es gilt:

F [w]=exp2 mit

2 x

, falls x

1 exp2(x)=

, sonst.

w berechnet also die Zweierpotenz, wenn man natürliche Zahlen

1 eingibt. In den übrigen Fällen terminiert w nicht, und die berechnete Funktion ist undefiniert.

Die Aufgabe besteht nun darin, F für alle Programme w einer Programmiersprache

P=(L, F ) zu definieren. Natürlich kann man dies nicht für die unendlich vielen w explizit durchführen, vielmehr muß man den speziellen syntaktischen Aufbau von w ausnutzen und F induktiv über diesen Aufbau definieren. Man präzisiert also die Semantik aller

Sprachelemente isoliert voneinander, zunächst der elementaren Sprachelemente, dann der einzelnen Konstruktoren usw. Schließlich liegt für jedes Sprachelement das partielle

Verhalten der semantischen Funktion vor, und man kann für ein konkretes Programm

11-17 alle diese Teilfunktionen entsprechend des Programmaufbaus zusammensetzen und erhält so die Semantik des Gesamtprogramms.

Um die prinzipielle Vorgehensweise bei der Definition einer semantischen Funktion F zu verstehen, beschränken wir uns jedoch der Übersichtlichkeit halber im weiteren Verlauf auf die Semantikdefinition einer Teilmenge von ML, genannt µML.

11.2.1 Die Syntax von µML

In µML mögen nur folgende Typen und Konstrukte von µML zugelassen sein: der elementare Datentyp int

,

Ausdrücke über int

und bool

,

Wertdeklarationen der Form val x=E

mit einem arithmetischen

Ausdruck

E

vom Typ int

,

Funktionsdefinitionen der Form

fun

mit einem Parameter, eine spezielle Eingabeanweisung read

.

Die Syntax von µML lautet in Backus-Naur-Form:

<prog> ::= <decls> ; <call>

<decls> ::= <decl> ; <decls> |

ε

<decl> ::= <valuedecl> | <fundecl>

<valuedecl> ::= val

<id> = <arithexp>

<fundecl> ::= fun

<id> <id> = <exp>

<exp> ::= <arithexp> | <condexp>

<arithexp> ::= (<arithexp> <op> <arithexp>) | <const> |

<id> | <id> (<arithexp>)

<op> ::= + | - | * | /

<condexp> ::= if

<boolexp>

then

<exp> else

<exp>

<boolexp> ::= <arithexp>=<arithexp> | <arithexp>

<arithexp> | true

| false

<id> ::= "Bezeichner nach üblichen Konventionen"

<const> ::= "ganzzahlige Konstante"

<call> ::= ( read

(<id>); <arithexp>).

11.2.2 Umgebungen

Die Semantik etwa eines arithmetischen Ausdrucks ist sein Wert. Um also die Semantik eines Ausdrucks z.B. der Form

2+x

11-18 zu ermitteln, müssen die Werte aller Objekte des Ausdrucks entsprechend der beteiligten Operationen verknüpft werden. Die Werte von Konstanten sind unmittelbar abzulesen, wie ermittelt man jedoch die Werte von Bezeichnern? Offenbar muß man eine Art

Symboltabelle mitführen, aus der man die aktuelle Bindung jedes Bezeichners an jeder

Programmstelle ablesen kann. Da sich Bindungen im Laufe eines Programms, etwa durch Neudefinition, ändern können, benötigt man ferner Operationen, um den Inhalt der

Symboltabelle ebenfalls ändern zu können.

Diese Überlegungen führen auf den Begriff der sog. Umgebung.

Umgebungen u sind Abbildungen von der Menge der möglichen Bezeichner in die

Menge der möglichen Werte; in µML sind das einerseits die elementaren Werte und andererseits Funktionsrümpfe (=Ausdrücke). Wir definieren also: u: {Bezeichner}

→ int

∪(

{Bezeichner}

×

[ int

→ int ]

×

{Umgebungen}).

Zu jedem Bezeichner x liefert u den aktuellen Wert u(x), sofern x gebunden ist, anderenfalls undefiniert

. Ist x an einen elementaren Wert gebunden, so ist u(x) dieser

Wert, ist x an eine Funktion gebunden, so ist u(x) ein Tripel

(p,r,u').

Hierbei ist p der Bezeichner des formalen Parameters von x, r der Funktionsrumpf von x

(also ein Ausdruck, der eine Funktion in [ int

→ int ] beschreibt) und u' die Umgebung, in der x deklariert wurde. u' wird benötigt, um das Konzept der statischen Bindung von ML formal korrekt zu formulieren: Wird nämlich x aufgerufen, so gelten für die Objekte im

Rumpf von x die Bindungen u' zum Zeitpunkt der Deklaration von x und nicht die Bindungen u zum Zeitpunkt des Aufrufs von x. Die Umgebung u' muß daher in dem Augenblick wiederhergestellt werden, wenn x aufgerufen wird.

Für später legen wir folgende Bezeichnungen fest: Zu einem Namen x, der in der aktuellen Umgebung u eine Funktion bezeichnet, also u(x)=(p,r,u'), liefere

π

den formalen Parameter p, also

π

(u(x))=p,

ρ

den Rumpf der Funktion, also

ρ

(u(x))=r, und

υ

die

Umgebung, in der x deklariert wurde, also

υ

(u(x))=u'.

Beispiel: Definiert man in der Umgebung u eine Funktion

E1

, so erhält man danach eine neue Umgebung u', in der für f gilt: u'( f

)=(p,

if B then

, u).

Umgebungen stellen wir meist als Menge von Gleichungen u={x

1

=w

1

,...,x n

=w n

} dar. Hierbei bedeutet x i

=w i

, daß der Bezeichner x i

in der Umgebung u den Wert w besitzt. Für y

≠ x i

, i=1,...,n gilt u(y)=

.

i

11-19

Im Laufe eines Programms können Bezeichner an unterschiedliche Werte gebunden werden. Um diese Veränderungen von Umgebungen übersichtlich formal zu beschreiben, definieren wir eine Operation + auf Umgebungen wie folgt: Sei u={x

1

=w

1

,...,x n

=w n

} und u'={y

1

=v

1

,...,y m

=v m

}.

Dann gilt: u"=u+u' mit u(x), falls u'(x)=

, u"(x)= u'(x), sonst.

In u" werden also die Bindungen der Bezeichner in u durch die Bindungen in u' überschrieben. Man beachte, daß + nicht kommutativ ist.

Beispiel: Sei u={x=2, y=3}, u'={x=(a, if a=0

,{y=4,z=0}), z=5}. Dann ist u"=u+u'={x=(a,

0 else 1

,{y=4,z=0}), y=3, z=5}.

Nun wollen wir die Semantik von µML definieren. Dabei gehen wir wie erwähnt induktiv vor, indem wir für die einzelnen Sprachelemente von µML semantische Funktionen definieren, die im Zusammenspiel geeignet sind, die Semantik von Programmen zu beschreiben.

Programme.

Wir beginnen bei Programmen und brechen die zugehörige semantische Funktion schrittweise auf semantische Funktionen für die Bestandteile der Programme herunter.

Die Bedeutung von Programmen beschreibt die semantische Funktion:

B : µML

[ int *

→ int ].

B bildet also wie erwartet Programmtexte in µML auf Funktionen ab, die einen Eingabeund einen Ausgabewert vom Typ int besitzen. B ist definiert durch

B [<decls>;( read (<id>);<arithexp>)](

α

)=

E [<arithexp>](( D [<decls>]

)+{<id>=

α

}).

Erläuterung: Die Bedeutung eines Programms der angegebenen Form

<decls>;( read

(<id>);<arithexp>) mit dem Eingabewert

α

erhält man wie folgt: Man stellt zunächst mittels D die Umgebung her, die die Deklarationen <decls> auf der leeren Umgebung

(noch kein Bezeichner gebunden) bewirken. Zu dieser Umgebung fügt man die Bindung des aktuellen Eingabewertes

α

an den Bezeichner <id> hinzu. In dieser neuen Umgebung wertet man anschließend mittels der semantischen Funktion E den Ausdruck <arithexp> aus und erhält das Ergebnis. D und E beschreiben wir im folgenden.

11-20

Deklarationen.

Die Bedeutung von Deklarationen beschreibt die semantische Funktion

D : {Deklarationen}

[{Umgebungen}

{Umgebungen}].

Eine Folge von Deklarationen bildet also unter D Umgebungen auf neue Umgebungen ab. D ist definiert durch:

D [ ]u=u,

D [<decl>;<decls>]= D [<decls>]

°

D [<decl>].

Erläuterung: Bilde zu einer Umgebung zunächst die neue Umgebung, die durch die

Deklaration <decl> hergestellt wird, und wende darauf die Umgebungstransformation von <decls> an. Auf diese Weise werden die einzelnen Deklarationen schrittweise abgearbeitet und die jeweiligen Umgebungen aufgebaut.

Wertdeklarationen:

D [ val <id>=<arithexp>]u=u+{<id>= E [<arithexp>]u}.

Erläuterung: Eine Wertdeklaration verändert eine vorliegende Umgebung u durch

Hinzufügen der Bindung von <id> an den mittels der semantischen Funktion E (s.u.) in der Umgebung u bestimmten Wert des Ausdrucks <arithexp>.

Funktionsdeklarationen:

D [ fun <id1> <id2> = <exp>]u=u+{<id1>=(<id2>,<exp>,u)},

Erläuterung: Eine Funktionsdeklaration verändert eine Umgebung u durch Hinzufügen der Bindung von <id1> an das Tripel bestehend aus dem formalem Parameter <id2>, dem Funktionsrumpf <exp> und der Umgebung, in der <id1> deklariert wurde. Dies ist u selbst.

Ausdrücke.

Die Bedeutung von Ausdrücken wird durch die semantische Funktion

E : {Ausdrücke}

[{Umgebungen}

→ int

∪ bool ] beschrieben: Sie wertet einen Ausdruck in einer Umgebung aus und liefert einen Wert aus int

oder bool

. Man definiert sie induktiv.

Für arithmetische Ausdrücke:

E [(<arithexp1> + <arithexp2>)]u=( E [<arithexp1>]u + E [<arithexp2>]u),

↑ ↑

Beachte: Hier handelt es sich um ein Funktionszeichen, hier um die zugehörige Funktion.

Analog lautet die Definition für die Operationen -,*,/.

Für boolesche Ausdrücke:

E [<arithexp1> = <arithexp2>]u= E [<arithexp1>]u = E [<arithexp2>]u,

E [<arithexp1>

<arithexp2>]u= E [<arithexp1>]u

E [<arithexp2>]u,

11-21

E [ true ]u=wahr,

E [ false ]u=falsch.

Für bedingte Ausdrücke:

E [ if <boolexp> then <exp1> else <exp2>]u=

E [<exp1>]u, falls E [<boolexp>]u=wahr,

E [<exp2>]u, falls E [<boolexp>]u=falsch.

Für Konstanten:

E [<const>]u=<const>.

Für Bezeichner:

E [<id>]u=u(<id>).

Für Funktionsanwendungen:

E [<id>(<arithexp>)]u=

E [

ρ

(u(<id>))]( D [ fun <id> (

π

(u(<id>)))=

ρ

(u(<id>)); val

π

(u(<id>))= E [<arithexp>]u]

υ

(u(<id>))),

Erläuterung: Die Semantik eines Funktionsaufrufs erhält man wie folgt: Man wertet den

Funktionsrumpf

ρ

(u(<id>)) aus. Das muß in der Umgebung

υ

(u(<id>)) geschehen, in der die Funktion <id> deklariert wurde. Diese Umgebung reichert man mittels D einerseits an um die Funktionsdeklaration fun

<id> ... selbst, denn <id> muß ja innerhalb von <id> bekannt sein (um Rekrusionen zu ermöglichen). Andererseits ergänzt man die Bindung des aktuellen Parameters <arithexp>, der in der Umgebung u, also vor Aufruf, mittels

E [<arithexp>]u ausgewertet wird, an den formalen Parameter

π

(u(<id>)), d.h. man deklariert den formalen Parameter in der Umgebung

υ

(u(<id>)) neu.

Für boolesche Ausdrücke:

E [<arithexp1> = <arithexp2>]u= E [<arithexp1>]u = E [<arithexp2>]u,

E [<arithexp1>

<arithexp2>]u= E [<arithexp1>]u

E [<arithexp2>]u,

E [ true ]u=wahr,

E [ false ]u=falsch.

Die Funktionen D [ a ], E [ a ] usw. heißen auch Denotationen von a . Die Funktionensemantik heißt danach auch denotationale Semantik.

Beispiele:

1) Wir bestimmen die Semantik eines einfachen Programms

P

.

P

sei val x

=1; val y

=

3

;

(read( z

);

(y*(z+x))

).

Für einen beliebigen Eingabewert

α

:int gilt:

11-22

B [ P ](

α

)= E [ (y*(z+x)) ](( D [ x =1; val y = 3 ]

)+{z=

α

}).

Hierbei ist

D [ x

=1; val y

=

3

]= D [ y

=

3

]

°

D val x

=1].

Im einzelnen:

D [ x =1]

=

+{x= E [1]

}={x=1},

D [ y = 3 ]{x=1}={x=1}+{y=3}={x=1,y=3}.

Folglich gilt:

B [ P ](

α

)= E [ (y*(z+x)) ] {x=1,y=3,z=

α

}.

Setzen wir u={x=1,y=3,z=

α

}, so folgt:

E [ (y*(z+x)) ]u=( E [ y ]u* E [ (z+x) ]u)=( E [ y ]u*( E [ z ]u+ E [ x ]u))

=(u( y )*(u( z )+u( x )))=(3*(

α

+1)).

Damit ist bewiesen, daß die Semantik des Programms

P

, dargestellt durch die berechnete Funktion, lautet:

B [ P ](

α

)=3*(

α

+1).

2) Wir bestimmen die Semantik von

P'

: val c=1;

(read(x);f(x)).

Für einen beliebigen Eingabewert

α

:int gilt:

B [ P' ](

α

)= E [f(x)](( D [

Hierbei ist

D [

D [ else

°

D else val c=1].

)+{x=

α

}).

Im einzelnen:

D [

=

+{c= E [1]

}={c=1},

D [

Folglich gilt:

B [P'](

α

)= E [f(x)] {x=

α

, c=1, f=(x,

Setzen wir u={x=

α

, c=1, f=(x,

E [f(x)]u= E [

( D [ f(x-1); E [x]u]

υ

(u(f)))=

(*)

E [

( D [

E [ else

α

f(x-1); E [x]u]{c=1})=

, c=1, f=(x,

E [c]u=u(c)=1, falls E [x=0]u=wahr, d.h., falls u(x)=0,

11-23

=

E [f(x-1)]u, sonst.

Hierbei ist

(**) E [f(x-1)]u= E [

( D [

E [f(x-1)]u= E [

(

E

D [

[

f(x-1); E [x-1]u]

υ

(u(f)))=

f(x-1); E [x-1]u]{c=1})= else

α

-1, c=1, f=(x,

1),{c=1})}.

Nun betrachte man die Zeilen (*) und (**), die zu folgender Gleichung führen:

E [ if x=0

1, falls u(x)=0,

(***)

=

E [ if x=0 else

α then

Setzt man nun zur Abkürzung

F:= E [

G:= D [ u:= {x=

α

, c=1, f=(x, then so erhält man aus (***) folgende Funktionalgleichung

1, falls u(x)=0,

F(u)=

F(G(u)), sonst.

Gesucht ist in dieser Gleichung die Funktion F, welche die Semantik des Programmstücks beschreibt; G und u sind bekannt. Die semantische Analyse des Programms P' hat also nicht – wie in Beispiel 1 – zu einer expliziten Angabe der berechneten Funktion von P' geführt. Vielmehr haben wir nun eine Funktionsgleichung vorliegen, die erst aufzulösen ist.

Da es sich bei der obigen Gleichung für F um eine implizite Gleichung handelt, kann man sich fragen, ob es überhaupt Funktionen F gibt, die diese Gleichung erfüllen.

Dies ist gleichbedeutend mit der Frage, ob man dem analysierten Programm in dem entwickelten Kalkül der semantischen Funktionen tatsächlich eine Bedeutung zuordnen kann.

11-24

Analysiert man die Gleichung genauer, so stellt sich heraus, daß es sich um eine sog. Fixpunktgleichung 0 der Form

F=

τ

(F) handelt, wobei

τ

ein Funktional ist mit

1, falls u(x)=0,

τ

(F)(u)=

F(G(u)), sonst.

Die Lösung F dieser Fixpunktgleichung ist dann die gesuchte Semantik des Programms. Die obige Frage nach der Lösung der Gleichung ist damit auf das Problem reduziert, unter welchen Bedingungen solche Fixpunkte existieren und ob sie ggf.

eindeutig bestimmt sind. Die Eindeutigkeit ist besonders wichtig, denn die Semantik des Programmstücks (der Fixpunkt) sollte ja auf keinen Fall mehrdeutig sein. Ferner sollte der Fixpunkt auch berechenbar sein.

Diesem Problemkreis, dessen Lösung einen relativ hohen technischen Aufwand erfordert, können wir uns in dieser Vorlesung nicht widmen. Wir wollen uns stattdessen mit einem kurzen Abriß der Vorgehensweise begnügen, wie man den gesuchten Fixpunkt berechnet, ohne jedoch die zugrundeliegenden mathematischen Strukturen darzustellen oder das Verfahren Fixpunktberechnungsverfahren beweistechnisch herzuleiten.

Dies bleibt Vorlesungen über die Theorie der Programmierung vorbehalten.

11.2.3 Fixpunktberechnung

Wie gesehen ordnet die Funktionensemantik den Programmen einer Programmiersprache Funktionen F als Bedeutung zu, die teilweise – wie in Beispiel 2 aus 11.2.2 – durch implizite Fixpunktgleichungen der Form F=

τ

(F) bestimmt sind. Wie geht man vor, um diese Gleichungen nach F aufzulösen?

Hierzu muß man zunächst überlegen, welcher Art die Quell- und Zielbereiche sind, die den Funktionen F und

τ

zugrundeliegen, und welche Funktionen F als Lösungen nur in

Frage kommen können oder sollen. Sodann muß man den Bereichen, auf denen F und

τ operieren, eine geeignete Struktur aufprägen und für

τ

nur noch gewisse strukturverträgliche Abbildungen zulassen, so daß für solche

τ

ein eindeutig bestimmter Fixpunkt existiert und ermittelt werden kann.

0

Sei f: M

M eine Funktion. Jedes x

M mit f(x)=x heißt Fixpunkt von f.

Beispiel: Sei f: IN

IN definiert durch f(x)=Summe aller Teiler von x ohne x selbst. f besitzt Fixpunkte, z.B.

f(6)=1+2+3=6, f(28)=1+2+4+7+14=28.

11-25

Die grundlegenden Arbeiten zu diesem Problemkreis stammen von Dana S. Scott:

Outline of a mathematical theory of computation, 1970. Er schlägt als grundlegende

Struktur der Bereiche den vollständigen Verband vor. Später hat sich gezeigt, daß bereits die Struktur vollständiger partieller ordnungen (cpo) ausreicht.

Welche Intuition steckt hinter diesen partiellen Ordnungen? Die typischen Bereiche in der Funktionensemantik sind Datentypen wie int , bool usw.

τ

operiert auf Funktionenklassen über diesen Typen. Da die Funktionen auch partiell definiert sein können, empfiehlt es sich, die Datentypen zu vervollständigen, also um das "bottom"-Element

(Bedeutung: "undefiniert") zu erweitern, wie wir es bereits mehrfach getan haben. Partielle

Funktionen lassen sich dann bezgl. ihres Grades an Definiertheit ordnen. Kleinstes

Element bezgl. dieser Ordnung (hier mit

bezeiechnet) ist die total undefinierte Funktion

mit

(x)=

für alle x.

Zwei Funktionen f und g sind vergleichbar nach folgender Vorschrift: f

≤ g :<=> für alle x: (f(x)

≠⊥

=> f(x)=g(x)), d.h. g ist an mindestens sovielen Stellen definiert wie f und stimmt an den Stellen, an denen f definiert ist, mit f überein. Offenbar gilt dann:

⊥≤ f für alle f.

ist eine partielle Ordnung. Fordert man nun für die partielle Ordnung eine gewisse

Abschlußeigenschaft, nämlich daß jede Kette x

1

≤ x

2

≤ x

1

... ein Supremum (eine kleinste obere Schranke) besitzt, und für die Funktionale

τ

die Monotonie f

≤ g =>

τ

(f)

≤τ

(g) sowie weitere Eigenschaften, darunter die Stetigkeit, so kann man schließlich zeigen, daß ein Fixpunkt der Gleichung

F=

τ

(F) existiert. Im allgemeinen gibt es jedoch mehrere Fixpunkte, darunter aber nur genau

einen bezgl. der Ordnung

kleinsten Fixpunkt F

0

. F

0

ist also der Fixpunkt, der für die wenigsten Argumente definiert ist. Dieses F

0

definiert man als die gesuchte Semantik des Programms. F

0

läßt sich relativ einfach als Grenzwert

F

0

=lim n

→∞

τ n

(

) berechnen. Grundlage dieses Ergebnisses ist der Fixpunktsatz von Kleene, den wir im folgenden ohne Beweis angeben.

Satz: (Fixpunktsatz von Kleene)

Sei (M,

) eine vollständige partielle Ordnung und

τ

: M

M eine stetige Funktion. Dann besitzt

τ

einen bezgl. der Ordnung

kleinsten Fixpunkt F

0

, und es gilt

11-26

F

0

=lim n

→∞

τ n

(

).

Der Fixpunktsatz von Kleene liefert folgendes konstruktives Verfahren, um den Fixpunkt zu berechnen:

1. Schritt: Setze x

0

:=

.

2. Schritt: Wiederhole für n=0,1,2,...

x n+1

←τ

(x n

).

Gilt dann irgendwann x r

=x r+1

, so ist x r

=F

0

der gesuchte kleinste Fixpunkt. Tritt dieser Fall nicht ein, so terminiert die Berechnung nicht, jedoch approximiert der Zwischenwert x n den gesuchten kleinsten Fixpunkt beliebig genau. Häufig gelingt es dann, den Fixpunkt

F

0

=lim n

→∞

x n durch Grenzwertbetrachtungen zu gewinnen.

Nun können wir auch die Fixpunktgleichung aus Abschnitt 11.2.2 lösen, wenn wir uns vergewissert haben, daß der Fixpunktsatz von Kleene in dieser Situation anwendbar ist.

Tatsächlich gilt – und dies müssen wir hier ohne Beweis hinnehmen –, daß, wann immer die Bestimmung der semantischen Funktion eines Programms aus µML auf eine Fixpunktgleichung führt, die beteiligten Objekte die Voraussetzungen des Satzes von

Kleene erfüllen.

Folglich ist die implizite Funktionalgleichung der Form (s. letztes Beispiel 11.2.2)

E [

if x=0 then

]u

1, falls u(x)=0,

=

E [

if x=0 then

] {x=

α

-1, c=1, f=(x,

c else f(x-1)

,{c=1})}, sonst, die Ausgangspunkt aller unserer Überlegungen zur Semantik war, sinnvoll, und sie besitzt nach dem Satz von Kleene einen eindeutig bestimmten kleinsten Fixpunkt, der die Bedeutung von

E [ if x=0 c ] und damit des gesamten Programms definiert. Diesen Fixpunkt wollen wir im folgenden berechnen.

Beispiele:

1) Zur Abkürzung setzen wir

F:= E [

G:= D [ u:= {x=

α

, c=1, f=(x,

Dann ist folgende Gleichung zu lösen:

11-27

1, falls u(x)=0

F(u)=

F(G(u)), sonst.

Setzt man

1, falls u(x)=0

τ

(F)(u)=

F(G(u)), sonst, so kann man den Fixpunktsatz von Kleene verwenden und rechnen:

1, falls u(x)=0 1, falls

α

=0

τ

(

)(u)= =

(G(u)), sonst

, sonst.

τ 2( ⊥

)(u)=

1, falls u(x)=0 1, falls

α

=0

= 1, falls G(u)(x)=0

(G(G(u))), sonst

1, falls

α

=0

= 1, falls

α

-1=0

, sonst.

τ

(

)(G(u)), sonst

Hierbei ist

G(u)= D [ E [x-1]u}=u+{x= E [x-1]u-1}=u+{x=

α

-1}

={x=

α

-1, c=1, f=(x, und folglich

G(u)(x)=

α

-1.

Also gilt:

1, falls

α

=0

τ

2

(

)(u)= 1, falls

α

=1

, sonst.

1, falls u(x)=0 1, falls

α

=0

τ 3( ⊥

)(u)= = 1, falls G(u)(x)=0

τ 2( ⊥

)(G(u)), sonst 1, falls G

2

(u)(x)=0

, sonst

Hierbei gilt – wie oben – G

2

(u)(x)=

α

-2. Also insgesamt

1, falls

α

=0

τ

3

(

)(u)= 1, falls

α

=1

1, falls

α

=2

, sonst.

Dann gilt offenbar allgemein:

1, falls 0

≤α≤ n-1,

τ n

(

)(u)=

1, falls

α

=0

= 1, falls

α

-1=0

1, falls

α

-2=0

, sonst.

11-28

, sonst.

Für den Grenzwert gilt dann

1, falls

α≥

0, lim n

→∞

τ n

(

)(u)= =: F

0

(

α

).

, sonst.

F

0

ist der kleinste Fixpunkt des Funktionals

τ

und die Semantik des Programms. Man

überzeuge sich von der Fixpunkteigenschaft durch Einsetzen von F

0

in die Gleichung

τ

(F)=F. Das Programm berechnet also die Funktion

1, falls

α≥

0,

F

0

(

α

)=

, sonst.

2) Wir bestimmen die Semantik des Programms P"

(read(y); f(y)) .

Für einen beliebigen Eingabewert

α

:int gilt:

B [

P"

](

α

)= E [f( y

)](( D [ fun

= E [f( y

)](

+{f=(x, if x=0 then

]

)+{y=

α

})

0

,

)}+{y=

α

})

= E [f( y

)]{y=

α

,f=(x, if x=0 then

,

)}.

Setzen wir zur Abkürzung u={y=

α

,f=(x, if x=0 0

,

)}.

Dann gilt:

E [f( y

)]u= E [

if x=0 0

]

E [

( D [ f x= if x=0 val x= E [ y

]u]

υ

(u(f)))= if x=0 0

] ( D [

if x=0 val x= E [ y

]u]

)=

E [ if x=0 0

]{x=

α

, f=(x,

if x=0 then

,

)}.

Mit u'={x=

α

, f=(x,

if x=0 0

,

)} gilt dann:

(*) E [ if x=0 0

]u'

E [

0

]u'=0, falls E [x=0]u'=wahr, d.h., falls u'(x)=

α

=0,

=

E [ f x

]u', sonst.

Hierbei ist

(**) E [f

x

]u'= E [ if x=0 0

]

( D [ f x= if x=0 val x= E [x]u']

υ

(u'(f)))=

E [f

x

]u'= E [ if x=0 0

]

( D [ f x= if x=0 val x= E [x]u']

)=

E [ if x=0

]u'.

Nun betrachte man die Zeilen (*) und (**), die zu folgender Gleichung führen:

11-29

0, falls u'(x)=0,

(***) E [ if x=0 0 ]u'=

Setzt man nun zur Abkürzung

F:= E [ then 0 else x ],

E [ if x=0 0 ]u', sonst.

so erhält man aus (***) folgende Funktionalgleichung

0, falls u'(x)=0,

F(u')=

F(u'), sonst.

Wir wenden den Satz von Kleene an und berechnen den Fixpunkt, indem wir setzen

0, falls u'(x)=0

τ

(F)(u')=

F(u'), sonst, so kann man den Fixpunktsatz von Kleene verwenden und rechnen:

0, falls u'(x)=0 0, falls

α

=0

τ

(

)(u)= =

(u'), sonst

, sonst.

τ

2

(

)(u)=

0, falls u'(x)=0

τ

(

)(u'), sonst

0, falls

α

=0

= 0, falls u'(x)=

α

=0 =

(u'), sonst

0, falls

α

, sonst.

=0

Also gilt:

0, falls

α

=0

τ

2

(

)(u)=

, sonst.

Wegen

τ

2

(

)(u)=

τ

(

)(u) ist F

0

:=

τ

(

) der kleinste Fixpunkt und die Semantik des Programms

P"

. Man überzeuge sich von der Fixpunkteigenschaft durch Einsetzen von

F

0

in die Gleichung

τ

(F)=F. Das Programm berechnet also die Funktion

0, falls

α

=0,

F

0

(

α

)=

, sonst.

Was this manual useful for you? yes no
Thank you for your participation!

* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project

Download PDF

advertisement