09.02.2014 Aufrufe

Ausarbeitung des „Interpreter“ Referats

Ausarbeitung des „Interpreter“ Referats

Ausarbeitung des „Interpreter“ Referats

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>Ausarbeitung</strong> <strong>des</strong> <strong>„Interpreter“</strong> <strong>Referats</strong><br />

Gliederung<br />

1. Programmiersprache<br />

1.2. Syntax<br />

1.2.1. Konkrete Syntax<br />

1.2.2. Abstrakter Syntax Baum (Abstrakte Syntax)<br />

2. Parser<br />

2.1. Syntaktische Struktur einer Sprache<br />

2.2. Parser Generator<br />

3. Interpreter<br />

4. Erweiterungen unserer Programmiersprache<br />

4.1. Bedingte Auswertung<br />

4.2. Lokale Bindungen<br />

4.3. Prozeduren und Closures


1. Programmiersprache<br />

Ein Interpreter beschäftigt sich damit ein Programm auszuführen.<br />

Aber zuerst müssen wir das Programm schreiben und dafür<br />

benötigen wir eine Programmiersprache. Es gibt viele Aspekte<br />

unter denen wir eine Programmiersprache analysieren können,<br />

aber wir werden uns nur mit der Syntax einer Sprache<br />

beschäftigen.<br />

1.2. Syntax:<br />

System von Regeln, das die Form einer (Programmier-)<br />

Sprache beschreibt. Die Syntax ist nicht die Bedeutung der<br />

Sprache das ist die Semantik.<br />

Man unterscheidet zwischen konkrete und abstrakte Syntax einer<br />

Programmiersprache.<br />

1.2.1. Konkrete Syntax:<br />

Ist die Syntax der Sprache, so wie sie der<br />

Programmierer in den Computer tippt. (Der<br />

Programmtext, hello.scm oder hello.java). Die BNF<br />

Definition, für Kontext-freie-Grammatiken, beschreibt<br />

wie bestimmte Datentypen repräsentiert werden (z.B.<br />

Addition mit „+“, Multiplikation mit „*“ u.s.w.) und<br />

dafür benutzt sie die bestimmten Zeichenketten und<br />

Werte, erzeugt bei der Grammatik. = externe<br />

Repräsentation. Die konkrete Syntax wurde erfunden<br />

nur damit es für die Menschen leichter ist, ihre


Programme schreiben zu können und die abstrakte<br />

Syntax(= interne Repräsentation) zu verstehen.<br />

1.2.2. Abstrakte Syntax (Abstrakter Syntax Baum):<br />

Ist die konkrete Syntax als möglichst knapper Baum<br />

dargestellt. Terminals, wie Klammer, brauchen nicht<br />

gespeichert werden, weil die keine Information<br />

beinhalten. Um abstrakte Syntax für bestimmte<br />

konkrete Syntax bauen zu können, müssen wir jede<br />

Produktion und deren nonterminals der konkreten<br />

Syntax benennen.<br />

::=<br />

var-exp (id)<br />

Eine Produktion der Grammatik<br />

Wir haben diese Produktion der konkreten Syntax mit var-exp<br />

benannt und ihre nonterminlas mit id.<br />

Die folgenden Beispiele zeigen wie die konkrete Syntax als<br />

abstrakt aussieht. Man sieht ganz genau wie die unnötigen<br />

Terminals von dem Programm herausgenommen worden sind.<br />

Im Beispiel 2 ist es nicht so ganz klar. Stellt euch vor den ganzen<br />

Text weg, das was übrig bleibt sind die Elemente, genommen<br />

von der konkreten Syntax, nämlich: add-prim (steht für das „+“<br />

Zeichen), 3 und 4. Der Text ist von dem Interpreter (sein eigenen<br />

Programmtext).


Beispiel: konkrete<br />

abstrakte Syntax<br />

Beispiel 1: Konkret Abstrakt<br />

(+7(+(3 5))) +<br />

+ 7<br />

3 5<br />

Beispiel 2:<br />

+(3,4)<br />

a-programm(<br />

primapp-exp(add-prim List(<br />

lit-exp(3)<br />

lit-exp(4))))<br />

oder<br />

a-program<br />

primapp-exp<br />

add-prim ()<br />

lit lit<br />

3 4<br />

4<br />

2. Parser<br />

Wenn wir ein Programm schreiben, schreiben wir es in konkrete<br />

Syntax. Aber wie wir schon geklärt haben ist die konkrete Syntax<br />

nur für uns (die Menschen) gedacht. Damit der Interpreter uns<br />

verstehen kann, was wir programmiert haben muss unser<br />

Programm in abstrakte Syntax umgewandelt werden, die der<br />

Interpreter schon versteht. Diese Umwandlung macht der Parser:


konkrete<br />

Syntax<br />

Parser<br />

abstrakter<br />

Syntax Baum<br />

Dem Parser werden Tokens(=Reihenfolge von Zeichen(Worte,<br />

Zahlen, Sonderzeichen…)) als Input gegeben und er organisiert<br />

sie in hierarchisch-syntaktische Struktur(Expressions,<br />

Statements, Blöcke…). Er formt die syntaktische Struktur einer<br />

Sprache.<br />

2.2. Es besteht die Möglichkeit den Parser selbst zu<br />

programmieren, aber heut zu Tage macht man das nicht.<br />

Dafür gibt es ein anderes Programm der Parser-Generator:<br />

hat als Input lexikalische Spezifikation und Grammatik,<br />

und produziert als Output scanner und parser für diese.<br />

3. Interpreter<br />

Nachdem der Parser unser Programm in abstrakte Syntax<br />

umgewandelt hat übergibt er sie dem Interpreter. Der Interpreter<br />

liest sie und führt sie zeilenweise aus. Im Gegensatz zu einem<br />

Compiler wird kein Maschinencode erzeugt.<br />

konkrete<br />

Syntax Parser<br />

abstrakter<br />

Syntax Baum<br />

Interpreter<br />

Antwort<br />

Ein Interpreter kann nur mit abstrakter Syntax operieren!!!


Beim programmieren eines Interpreters ist sehr wichtig zu<br />

unterscheiden zwischen definierte und definierende Sprache!<br />

Definierte Sprache (oder source Sprache):<br />

Die Sprache, die wir mit dem Interpreter definieren.<br />

Definierende Sprache (oder host Sprache):<br />

Die Sprache, in der wir den Interpreter schreiben. In<br />

unserem Fall wird es Scheme sein.<br />

Was auch sehr wichtig ist die definierende Sprache sehr gut zu<br />

kennen und mit ihr gut umgehen zu können. Warum, werden wir<br />

später erfahren.<br />

Wie bauen wir unser Interpreter?<br />

Immer wenn wir was Neues in unserer Programmiersprache<br />

definieren wollen, müssen wir uns an einem bestimmten Plan<br />

(Definitionsplan) halten:<br />

1. Definition der konkreten Syntax: wie wollen wir, dass<br />

die Sachen aussehen;<br />

2. Definition der abstrakten Syntax;<br />

3. Implementieren in dem Programm.<br />

4.<br />

Fangen wir mit den Schritten 1 und 2:<br />

So sehen unsere konkrete und abstrakte Syntax aus:<br />

::= <br />

a-program (exp)<br />

::= <br />

lit-exp (datum)


::= <br />

var-exp (id)<br />

::= ({}*´)<br />

primapp-exp (prim rands)<br />

<br />

::= + | - | * | add1 | sub1|<br />

Wir haben definiert, dass ein Programm nicht anderes außer ein<br />

Expression ist und ein Expression kann die folgenden Sachen<br />

sein: Zahl (lit-exp), Variable (var-exp) oder eine primitive<br />

Anwendung (primapp-exp). Für primitive Anwendungen<br />

definieren wir die Addition, Subtraktion, Multiplikation, addiere<br />

1 und subtrahiere 1.<br />

In unserer Sprache haben wir auch die Variablen definiert und<br />

damit wir richtig mit denen umgehen können, müssen wir noch<br />

ein Begriff klären: die Umgebung einer Funktion.<br />

Jede Funktion, die variablen beinhaltet, hat ihre eigene<br />

Umgebung für sie. In der Umgebung werden die Werte und<br />

Typen der Variablen gespeichert, damit die Funktion ausführbar<br />

wird.<br />

(define f (lambda (x)<br />

(let (y 10)<br />

(in (+ (x y))))))<br />

(f(5))<br />

Umgebung<br />

x 5<br />

y 10


Jetzt können wir schon mit der Implementierung anfangen. Und<br />

so sieht der Interpreter für unsere einfache Programmiersprache:<br />

(define eval-program<br />

(lambda (pgm)<br />

(cases program pgm<br />

(a-program (body)<br />

(eval-expression body (initenv))))))<br />

(define eval-expression<br />

(lambda (exp env)<br />

(cases expression exp<br />

(lit-exp (datum) datum)<br />

(var-exp (id) (apply-env env id))<br />

(primapp-exp (prim rands)<br />

(let ((args (eval-rands rands<br />

env)))<br />

(apply-primitive prim args)))<br />

)))<br />

Die Funktion init-env, deren Implementierung hier nicht<br />

gezeigt ist, dient dazu eine leere Umgebung zu erzeugen für das<br />

Programm mit Körper body. Danach wird evalexpression<br />

so oft aufgerufen bis wir alle einzelnen Elemente<br />

von body haben und eine eventuelle Anwendung auf denen<br />

ausgeführt haben.


4. Bis jetzt ist unsere Sprache ganz einfach und das wollen wir<br />

mit ein paar Erweiterungen verändern.<br />

4.1. Bedingte Auswertung – if Anweisung<br />

Wie wir alle wissen ist die if Anweisung eine<br />

Fallunterscheidung und damit wir die Auswertung<br />

durchführen zu können brauchen wir die boolesche Werte, um<br />

zu entscheiden welchen Weg wir nehmen sollen. Um das<br />

Hinzufügen von Bohlesche-Werte zu unserer Sprache zu<br />

vermeiden, werden wir sie als Integer-Werte kodiert. Dafür<br />

implementieren wir die folgende Hilfsfunktion:<br />

(define true-value?<br />

(lambda (x)<br />

(not (zero? x))))<br />

Mit dieser Funktion sagen wir, dass die Null als falsch<br />

interpretiert werden soll und alle andere Werte als richtig.<br />

Hier sind 2 Beispiele wie unser if aussehen wird und ihre<br />

Ausgabewerte:<br />

- -> if 1 then 2 else 3<br />

2<br />

- -> if –(3, + (1, 2)) then 2 else 3<br />

3<br />

Nachdem wir das Problem mit den booleschen Werten schon<br />

beseitigt haben, bleibt uns nur übrig die 3 Schritte <strong>des</strong><br />

bestimmten Definitionsplans zu folgen um die if Anweisung<br />

zu unserer Sprache hinzufügen.


Schritt 1. und 2.: Definition der konkreten und abstrakten<br />

Syntax:<br />

::= if then else <br />

if-exp (test-exp true-exp false-exp)<br />

Die Funktion if, in unserer Sprache, hat 3 formale Parameter.<br />

Nach der Bewertung <strong>des</strong> test-exps wird entschieden ob<br />

der true-exp oder der false-exp ausgewertet werden<br />

soll.<br />

Schritt 3.: Implementierung in eval-expression:<br />

(if-exp (test-exp true-exp false-exp)<br />

(if (true-value? (eval-expression test-exp env))<br />

(eval-expression true-exp env)<br />

(eval-expression false-exp env)))<br />

Dieser Code beinhaltet die if Form von der definierenden<br />

Sprache um die if Form der definierten Sprache zu<br />

implementieren. Deshalb ist es so wichtig, dass man mit der<br />

definierenden Sprache gut umgehen kann.<br />

4.2. Lokale Bindungen – let Anweisung<br />

Beispiel:<br />

let x = 5<br />

y = 6<br />

in +(x, y)<br />

Das ist ein Beispiel wie wir wollen, dass unsere let<br />

Anweisung aussieht.


Bei lokalen Bindungen innere Deklarationen beschatten, oder<br />

schaffen Löcher in dem Körper von, äußere Deklarationen.<br />

D.h. ein variablen Objekt wird immer mit der meist näheren<br />

lexikalischen Bindung gebunden. Dieser Gesetzt gilt bei allen<br />

Programmiersprachen mit Blockstruktur.<br />

Hier ist ein Beispiel bei dem die lokalen Bindungen gut zu<br />

sehen sind:<br />

let x = 1<br />

in let x = +(x, 2)<br />

in add1 (x)<br />

> 4<br />

Wie gewohnt folgen wir die Schritte in dem Definitionsplan:<br />

Schritt 1. und 2.:<br />

::= let { = }* in<br />

let-exp (ids rands body)<br />

Unser let ist so gebaut, dass er die Werte <strong>des</strong> Operanden den<br />

Identifikatoren, im Ausdruck body, übergibt.<br />

<br />

Schritt 3.:<br />

(let-exp (ids rands body)<br />

(let ((args (eval-rands rands env)))<br />

(eval-expression body (extend-env<br />

ids args env)))


Dieser Code beinhaltet die let Form der definierenden<br />

Sprache um die let Form der definierten Sprache zu<br />

implementieren.<br />

4.3. Prozeduren und Closures<br />

Wenn eine Prozedur aufgerufen wird, wird ihr Körper in einer<br />

Umgebung ausgeführt, die die formalen Parameter der<br />

Funktion mit den Argumenten der Applikation bindet.<br />

Beispiel:<br />

let x = 5<br />

in let f = proc (y, z) +(y, -(z, x))<br />

x = 28<br />

in (f 2 x)<br />

Die Funktion f wird in der Umgebung ausgeführt, die y mit 2,<br />

z mit 28 und x mit 5 bindet. z wird mit 28 gebunden, weil x =<br />

28 die meist nähere lexikalische Bindung von x, zu in (f 2<br />

x), ist.<br />

Wir wissen was Prozeduren sind, aber was sind Closures?<br />

Definition:<br />

Um eine Prozedur ihre Bindungen, die ihre freie Variablen am<br />

Prozedur-Erzeugung hatten, behalten zu können, braucht sie<br />

ein geschlossenes Packet, unabhängig von der Umgebung, in<br />

der sie ausgeführt wird! So ein Packet nennt man Closure.<br />

Eine Closure muss den Körper, die Namen der formalen<br />

Parameter und die Bindungen der freien Variablen der


Prozedur beinhalten. Es ist günstiger und praktischer die<br />

ganze Deklarationsumgebung in der Closure zu speichern und<br />

nicht nur die Bindungen der freien Variablen. Wenn wir nur<br />

die Bindungen in der Closure speichern würden und die<br />

Prozedur in einer fremden Umgebung ausführen, könnte sie<br />

mit fremdem Körper ausgeführt werden.<br />

Man sagt, dass eine Prozedur geschlossen über oder<br />

geschlossen in ihrer Deklarationsumgebung ist.<br />

Hier sind 2 Beispiele wie eine Closure gebaut wird und wie<br />

sie aussieht:<br />

Beispiel 1:<br />

(define f<br />

(lambda (x)<br />

Freie Variablen<br />

(let (y 10)<br />

(lambda (z) (+ x y z)))))<br />

(define g<br />

(f 5))<br />

Gebundene Variable<br />

Beispiel 2:<br />

(define g (33))<br />

x 5<br />

Closure y 10 , z, (+ x y z)<br />

z 33<br />

env ids body<br />

Bindungen Namen Körper<br />

der der der<br />

freien formalen Prozedur<br />

Variablen Parameter


Bis jetzt kann unsere Sprache nur Integer-Werte zurückgeben.<br />

Nachdem wir auch die Prozeduren definiert haben wollen wir,<br />

dass sie auch erste-Klasse Werte werden. Dafür müssen wir,<br />

aber auch neue Rückgabewerte zu der sprache definieren:<br />

ProcVal.<br />

Definition:<br />

ProcVal ist die Menge von Werten, die die Prozeduren<br />

repräsentieren. Das Interface <strong>des</strong> Datentyps ProcVal besteht<br />

aus:<br />

1. Closure – der beschreibt wie die Prozedur Werte<br />

gebaut werden sollen.<br />

2. apply-procval – Funktion, die beschreibt wie die<br />

Prozedur Werte verwendet werden sollen.<br />

Implementationene von procval und apply-procval:<br />

(dafine-datatype procval procval?<br />

(closure<br />

(ids (list-of symbol?))<br />

(body expression?)<br />

(env environment?)))<br />

ProcVal ist eine Menge von Werten und nicht nur ein Wert,<br />

weil der Wert einer Prozedur aus 3 verschiedene Elemente<br />

besteht: ids, body, env.<br />

(define apply-procval<br />

(lambda (proc args)<br />

(cases procval proc


(closure (ids body env)<br />

(eval-expression body (extend-<br />

env ids args env))))))<br />

Eine Closure wird in procval erzeugt und in apply-procval<br />

ausgeführt. Eine Prozedur wird in eval-expression<br />

ausgewertet:<br />

(...<br />

(app-exp (rator rands)<br />

(let ((proc (eval-expression rator<br />

env))<br />

(args (eval-rands rands<br />

env)))<br />

(if (procval? Proc)<br />

(apply-procval proc args)<br />

(eopl:error eval-expression<br />

„Attempt to apply nonprocedure<br />

~s“ proc))))<br />

...)<br />

Nachdem wir schon alle nötige Teile für die<br />

Prozedurendefinition definiert und implementiert haben,<br />

müssen wir die allgemeine konkrete und abstrakte Syntax<br />

definieren:<br />

::= proc ({}*´) <br />

proc-exp (ids body)


::= ( {}*)<br />

app-exp (rator rands)<br />

Schritt 3.: Implementieren in eval-expression:<br />

(proc-exp (ids body)<br />

(closure ids body env))<br />

Jetzt haben wir die volle Implementierung von den<br />

Prozeduren und deren Werte. Das folgende Beispiel zeigt die<br />

Anwendung aller Co<strong>des</strong>.<br />

(eval-expression <br />

env0)<br />

(eval-expression <br />

env1)<br />

wo env1 = [x = 5]env0<br />

(eval-expression env2)


wo env2 =<br />

[x = 38,<br />

f = (closure (y z) env1);<br />

g = (closure (u) env1)<br />

] env1

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

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!