Einleitung | Operationale Semantiken | ungetyptes λ-Kalkül | Einfach getyptes λ-Kalkül | Erweiterungen | Ausblick/Sourcen/Literatur



Operationale Semantiken

Es gibt verschiedene Ansätze die Semantik einer Programmiersprache und ihr Typsystem zu beschreiben. Wir werden drei-einhalb Beschreibungstypen kennenlernen und die sogenannten Operationalen Semantiken am Beispiel einer einfachen Sprache näher erklären. Operationale Semantiken werden uns in den folgenden Kapiteln immer begleiten.

Ansätze zur Beschreibung der Semantik von Programmiersprachen

Übersetzersemantik:
Beschreibt die Semantik einer Programmiersprache durch Übersetzungsregeln in eine andere Programmiersprache mit bereits bekannter Semantik. Diese Beschreibungsart ist somit nicht vollkommen eigenständig, sondern bedingt immer das es bereits eine Beschreibung für irgendeine andere Sprache gibt. Sie ist allerdings nützlich, wenn wir zum Beispiel eine realistische Programmiersprache mit Hilfe von einigen Übersetzungregeln auf ihren Kern reduzieren wollen. So könnte man zum Beispiel eine Übersetzersemantik für die Sprache C erfinden, die sämtliche for-Schleifen in while-Schleifen überführt und if-Anweisungen ohne else-Teil um einen leeren else-Teil ergänzt um so eine einfachere Kernsprache zu erhalten.

Operationale Semantik (Interpretersemantik):
Beschreibung der Semantik einer Programmiersprache mit Hilfe von schrittweisen Verarbeitungsregeln für Terme dieser Sprache. Wird im nächsten Teil genauer betrachtet.

Denotationale Semantik (Funktionensemantik):
Beschreibt die Semantik einer Anweisung mit Hilfe einer Abbildungsfunktion (F[a] : Z → Z), die einer Anweisung a eine Transformation des Programmzustands auf den Folgezustand zuordnet. Beispiel: F[x:=y+1] (1,3) = (4,3)

Axiomatische Semantik (Pradikatensemantik):
Ist die abstrakteste der betrachteten Beschreibungsformen. Sie beschreibt die Semantik von Konstrukten einer Sprache mit Hilfe von Vor- und Nachbedingungen ({P}a{Q}), ohne dabei anzugeben, wie die Transformation des Programmzustands erfolgt. Beispiel: {x>=1} x:=x-1 {x>=0}

Beispiel zur operationalen Semantik

Wir wollen nun eine einfache Sprache für Boolsche Ausdrücke betrachten.

Die Sprache sei durch die folgende abstrakte Syntax beschrieben:

t ::= true
      false
      if t then t else t

Abstrakte Syntax, im Gegensatz zu konkreter Syntax, meint hier zum Beispiel dass wir in konkreten Ausdrücken durchaus Klammern zur besseren Lesbarkeit (eine andere Funktion haben sie in dieser Sprache nicht) verwenden dürfen. Die Klammern sind aber nicht Bestandteil der abstrakten Syntax. Dort wird davon ausgegangen, dass wir bereits einen eindeutigen Programmbaum vorliegen haben.

Konkrete Ausdrücke in unserer Sprache sind beispielsweise:

true

if false then true else false

if (if false then false else true) then true else false

if true then false else (if false then true else true)

if (if false then true else false) then true else (if true then false else true)

Betrachten wir nun die Semantik dieser Sprache anhand einer operationalen Semantik:

Operationale Semantik - Beispiel 1

Ein Teilmenge aller Terme haben wir als Werte definiert. Werte sind spezielle Terme, die sich nicht weiter vereinfachen lassen. Die Auswertung (→) eines Terms beschreiben wir mit Hilfe von zwei sogenannten Axiomen (das sind Regeln ohne Prämisse) und einer Regel mit Prämisse. Axiom (1) besagt, dass if true then t2 else t3 jederzeit (für alle Terme t2 und t3) zu t2 ausgewertet werden kann. Axiom (2) besagt entsprechend, dass if false then t2 else t3 jederzeit zu t3 ausgewertet werden kann. Die Regel (3) besagt schliesslich, dass wenn der Term t1 zu t1' ausgewertet werden kann auch der Term if t1 then t2 else t3 zu if t1' then t2 else t3 ausgewertet werden kann.

In einem konkreten Beispiel würde die folgende Auswertung stattfinden:


if (if false then true else false) then true else (if true then false else true)
-> if (if false then true else false) then true else (if true then false else true)
-> if false then true else (if true then false else true)
-> if true then false else true
-> false

Zunächst wird mit der Regel (3) der Teilausdruck (if false then true else false) zur weiteren Verabeitung ausgewählt und mit Axiom (2) zu false ausgewertet. Danach wird der äußere Ausdruck mit Axiom (2) zu if true then false else true ausgewertet. Dieser Ausdruck wird dann mit Axiom (1) zu false ausgewertet. Auf diesen Ausdruck läßt sich keine Auswertungsregel mehr anwenden. Ein Term der nicht weiter vereinfacht werden kann wird als Term in Normalform bezeichnet.

Für unsere Sprache gelten die folgenden Eigenschaften:

Werte sind in Terme in Normalform:
Auf einen Wert trifft nie irgendeine Auswertungsregel zu. Diese Eigenschaft ist praktisch schon ein definitorisches Kriterium. Jede Sprache die diese Eigenschaft nicht hat, ist kaputt. In unserem Beispiel wäre es ziemlich schlimm, wenn wir beispielsweise aus true false ableiten könnten.

Jeder Term in Normalform ist ein Wert:
Läßt sich ein Term nicht weiter vereinfachen, handelt es sich um einen Wert, in unserem Fall um true oder false. Wir werden Sprachen kennen lernen in denen nicht jeder Term in Normalform ein Wert ist, sondern auch die Möglichkeit besteht, dass es sich bei einem Term in Normalform um einen Fehlerterm handelt, der einen Laufzeitfehler (zum Beispiel eine Exception) repräsentiert.

Die Auswertung (→) erfolgt deterministisch:
Auf keinen Term läßt sich mehr als eine Auswertungsregel anwenden. In unserem Beispiel könnten wir zum Beispiel nicht zuerst den Teilterm (if true then false else true) auswerten. Es ist nur diese eine Auswertungsreihenfolge möglich. Diese Eigenschaft gilt nicht für jede Sprache. Ein prominentes Beispiel ist die Programmiersprache C in der die Auswertungsreihenfolge von Funktionsargumenten undefiniert ist.

Jeder Term hat eine Normalform:
Jeder Term dieser Sprache kann nach einer endlichen Folge von Schritten in eine Normalform überführt werden. Man könnte auch sagen jedes Programm in unserer Sprache terminiert. Programmiersprachen haben diese Eigenschaft in der Regel nicht. In jeder Sprache die mächtig genug ist um eine universelle Turingmaschine zu simulieren, gibt es zwangsläufig auch Programme die nicht terminieren.


Steuerung der Auswertungsreihenfolge

In der oben angegebenden operationalen Semantik gilt die folgende Auswertungsreihenfolge: Es wird zunächst die Bedingung ausgewertet und danach abhängig von der Normalform der Bedingung entweder der then-Term oder der else-Term. Dies entspricht der üblichen Semantik von bedingten Ausdrücken in Programmiersprachen. Wir können aber auch jede beliebige Auswertungsreihenfolge erzwingen, ohne dabei die oben genannten Eigenschaften, insbesondere Determinismus, zu gefährden.

Die folgende operationale Semantik erzwingt eine andere Auswertungsreihenfolge:

Operationale Semantik - Beispiel 2

In dieser operationalen Semantik wurden an einigen Stellen die Metavariablen t1, t2 und t3, die für beliebige Terme stehen, durch die Metavariablen v1, v2 und v3, die für Werte (values) stehen, ausgetauscht. Dadurch, dass wir an einigen Stellen Werte gefordert haben, haben wir die folgende Auswertungsreihenfolge erzwungen: Es wird zunächst der then-Term, dann der else-Term, dann erst die Bedingung ausgewertet, erst dann kann eines der Axiome (1') oder (2') angewendet werden.

Unten haben wir nochmal den bereits bekannten Term if (if false then true else false) then true else (if true then false else true) nach den neuen Auswertungsregeln ausgewertet. Es ist dabei nur die folgende Reihenfolge möglich : Regel (5), Axiom (1'), Regel (3'), Axiom (1'), Axiom (2'):


if (if false then true else false) then true else (if true then false else true)
-> if (if false then true else false) then true else (if true then false else true)
-> if (if false then true else false) then true else false
-> if (if false then true else false) then true else false
-> if false then true else false
-> false



Implementierung in Haskell


module B (Term,eval)
  where

data Term = TmTrue | TmFalse | TmIf Term Term Term 
  deriving Show

eval                     :: Term -> Term
eval (TmIf TmTrue  t2 _ ) = eval t2                      -- Axiom 1
eval (TmIf TmFalse _  t3) = eval t3                      -- Axiom 2
eval (TmIf t1      t2 t3) = eval (TmIf (eval t1) t2 t3)  -- Regel 3
eval t                    = t                            -- Normalform  

Die Implementierung in Haskell entspricht praktisch direkt unserer ursprünglichen operationalen Semantik. Terme werden durch den Datentyp Term repräsentiert. Term hat die Typkonstruktoren TmTrue, TmFalse und TmIf Term Term Term. Die Auswertungsfunktion (→) haben wir eval genannt.

Einzig gewöhnungsbedürftigt ist die neue Notation für Terme so steht TmIf TmFalse TmTrue TmTrue für den Term if false then true else true. Dies ist aber nicht problematisch, da wir uns mit der abstrakten Syntax von Termen und nicht mit ihrer konkreten Syntax befassen. Wir gehen also hier davon aus, dass uns bereits ein Programmbaum vorliegt. Wie man von einer konkreten Syntax zu einem Programmbaum kommt ist Gegenstand der Vorlesung Compilerbau und soll uns an dieser Stelle nicht weiter interessieren.

Ein konkrete Session im ghci könnte wie folgt aussehen (Benutzereingaben kursiv):


Prelude> :load b
Compiling B                ( b.hs, interpreted )
Ok, modules loaded: B.
*B> eval (TmIf (TmIf TmFalse TmTrue TmFalse) TmTrue (TmIf TmTrue TmFalse TmTrue))
TmFalse
*B>