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