Blatt 05: Interpreter
(10 Punkte)
Zusammenfassung
Ziel dieses Aufgabenblattes ist die Erstellung eines Tree-Walking-Interpreter mit ANTLR für eine Lisp-artige Sprache.
Methodik
Sie finden im Sample Project zwei Grammatiken (MiniLispA, MiniLispB), die (teilweise) zu der Zielsprache auf diesem Blatt passen. Analysieren Sie beide Grammatiken und entscheiden Sie sich für eine der beiden Varianten. Vervollständigen Sie diese bzw. passen Sie diese an.
Erstellen Sie mit dieser Grammatik und ANTLR wieder einen Lexer und Parser.
Es ist empfehlenswert, den Interpreter dreistufig zu realisieren:
- Einlesen aus einer Datei mit Lisp-Code und Parsen der Inhalte
- Aufbauen der Symboltabelle und Durchführung der semantischen Analyse
- Ablaufen des Parse-Tree/AST und Auswerten der Ausdrücke (Interpretation)
Sprachdefinition
Ein Programm besteht aus einem oder mehreren Ausdrücken (Expressions). Die Ausdrücke haben
eine spezielle Form: Sie sind sogenannte S-Expressions. Dies sind entweder Literale der Form
x oder einfache listenartige Gebilde der Form (. x y), wobei der . eine Operation (oder
Funktion) darstellt und x und y selbst wieder S-Expressions sind.
Die einfachste Form sind dabei Literale mit konkreten Werten der drei Datentypen Integer,
String und Boolean:
42 ;; Integer
"hello" ;; String
true ;; Boolean
false ;; BooleanFür eine Variable foo wäre das Folgende ebenfalls eine S-Expression:
foo ;; Variable foo(Über ;; wird ein Kommentar eingeleitet, der bis zum Ende der Zeile geht.)
Komplexere Ausdrücke werden über die Listenform gebildet:
(+ 1 1) ;; 1 + 1
(/ 10 3) ;; 10 / 3
(+ 1 2 3 4) ;; 1 + 2 + 3 + 4
(+ (+ (+ 1 2) 3) 4) ;; (((1 + 2) + 3) + 4)
(/ (+ 10 2) (+ 2 4)) ;; ((10 + 2) / (2 + 4))In der listenartigen Form ist der erste Eintrag der Liste immer eine Operation (oder ein Funktionsname), danach kommen je nach Operation/Funktion (die Arität muss passen!) entsprechende Einträge, die als Parameter für die Operation oder Funktion zu verstehen sind.
Die Ausdrücke sind implizit von links nach rechts geklammert, d.h. der Ausdruck (+ 1 2 3 4)
ist syntactic sugar für (+ (+ (+ 1 2) 3) 4).
Eingebaute Funktionen
Es gibt zwei Funktionen, die fest in der Sprache integriert sind.
Mit der eingebauten Funktion print kann der Wert eines Ausdrucks auf der Konsole ausgegeben
werden:
(print "hello world")
(print "wuppie\nfluppie\nfoo\nbar")Die eingebaute Funktion str verknüpft ihre Argumente und bildet einen String. Falls nötig,
werden die Argumente vorher in einen String umgewandelt.
(str 42) ;; liefert "42" zurück
(str "wuppie" "fluppie" "foo" "bar") ;; liefert "wuppiefluppiefoobar" zurück
(str "one: " 1 ", two: " 2) ;; liefert "one: 1, two: 2" zurückOperatoren
Es gibt nur wenige vordefinierte Operatoren, diese mit der üblichen Semantik.
Vergleichsoperatoren
| Operation | Operator |
|---|---|
| Gleichheit | = |
| Größer | > |
| Kleiner | < |
Die Operanden müssen jeweils beide den selben Typ haben. Dabei sind String und Integer
zulässig. Das Ergebnis ist immer vom Typ Boolean.
Arithmetische Operatoren
| Operation | Operator |
|---|---|
| Addition | + |
| Subtraktion | - |
| Multiplikation | * |
| Division | / |
Die Operanden müssen jeweils beide den selben Typ haben. Dabei sind String und Integer
zulässig. Das Ergebnis ist vom Typ der Operanden.
Kontrollstrukturen (If-Else)
Die if-then-else-Abfrage gibt es mit und ohne den else-Zweig:
(if boolean-form
then-form
optional-else-form)(if (< 1 2)
(print "true") ;; then
(print "false")) ;; elseDabei kann jeweils nur genau eine S-Expression genutzt werden. Wenn man mehrere Dinge
berechnen möchte, nutzt man do:
(do
(print "wuppie")
(print "fluppie")
(print "foo")
(print "bar"))Beispiel:
(if (< 1 2) (do (print "true") (print "WUPPIE")) (print "false"))oder anders formatiert:
(if (< 1 2)
(do (print "true")
(print "WUPPIE"))
(print "false"))Variablen: Bindings mit def anlegen
(def x 42) ;; definiert eine neue Variable mit dem Namen "x" und dem Wert 42
x ;; liefert 42
(+ x 7) ;; liefert 49Funktionen mit defn definieren
;; name params body
(defn hello (n) (str "hello " n)) ;; Definition einer Funktion "hello" mit einem Parameter
(hello "world") ;; Aufruf der Funktion "hello" mit dem Argument "world"Lokale Scopes mit let
;; bindings use names here
(let (name value) (code that uses name))
(def x 99) ;; globale Variable x
(def y 101) ;; globale Variable y
(def z 42) ;; globale Variable z
(let (x 1 ;; lokales x mit Wert 1(verdeckt globales x)
y 2) ;; lokales y mit Wert 2
(+ x y z)) ;; 1+2+42
(defn hello
(n)
(let (l 42) ;; l is valid in this scope
(str "hello " n ": " l)
) ;; end of local scope
) ;; end of function definitionMit let können lokale Variablen erzeugt werden, die dann in dem jeweiligen Scope genutzt
werden können. Dies funktioniert wie in anderen Sprachen mit Scopes.
Rekursion
(defn fac (n)
(if (< n 2)
1
(* n (fac (- n 1)))))Da es kein while oder for gibt, müssen Schleifen über rekursive Aufrufe abgebildet werden.
Datenstrukturen
In unserer Sprache gibt es Listen:
(1 2 3) ;; Fehler!
(def v (1 2 3)) ;; Fehler!Das Problem daran ist, dass unsere S-Expressions zwar bereits listenartige Strukturen sind, der erste Eintrag aber als Operator oder Funktion interpretiert wird. Der Ausdruck oben würde beim Auswerten versuchen, die "Funktion" 1 auf den Argumenten 2 und 3 aufzurufen ...
Man braucht also eine Notation, die ein sofortiges Auswerten verhindert und nur die Liste an
sich zurückliefert. Dies erreicht man durch die Funktion list:
(list 1 2 3) ;; (1 2 3)
(def v (list 1 2 3)) ;; v = (1 2 3)
v ;; (1 2 3)Mit der Funktion nth kann man auf das n-te Element einer Liste zugreifen:
(nth (list "abc" false 99) 2) ;; 99Zusätzlich gibt es die beiden Funktionen head und tail, die das erste Element einer Liste
bzw. die restliche Liste ohne das erste Element zurückliefern:
(head (list 1 2 3)) ;; 1
(tail (list 1 2 3)) ;; (2 3)Aufgaben
A5.1: Grammatik und ANTLR (3P)
-
Erstellen Sie zunächst einige Programme in der Zielsprache. Diese sollten von einfachsten Ausdrücken bis hin zu komplexeren Programmen reichen. Definieren Sie beispielsweise eine Funktion, die rekursiv die Länge einer Liste berechnet.
Definieren Sie neben gültigen Programmen auch solche, die in der semantischen Analyse zurückgewiesen werden sollten. Welche Fehlerkategorien könnte es hier geben?
-
Definieren Sie für die obige Sprache eine geeignete ANTLR-Grammatik. Sie können dabei die Grammatik MiniLispA oder MiniLispB im Sample Project als Ausgangspunkt nutzen und diese anpassen und vervollständigen. Erzeugen Sie mithilfe der Grammatik und ANTLR einen Lexer und Parser.
-
Führen Sie die semantische Analyse durch: Sind alle Symbole bekannt, passen die Scopes?
A5.2: Tree-Walking-Interpreter (5P)
Bauen Sie einen Tree-Walking-Interpreter in Ihr Projekt ein.
Realisieren Sie die eingebauten Funktionen print und str dabei als native Funktionen.
Realisieren Sie list, nth, head und tail sowie def, let, defn, do und die
Operatoren und die Kontrollstrukturen geeignet.
Lösen Sie die als "syntactic sugar" bezeichneten Ausdrücke auf und transformieren Sie den
AST entsprechend: (+ 1 2 3 4) soll zu (+ (+ (+ 1 2) 3) 4) umgeformt werden. Analog für die
anderen Operatoren der Sprache (Vergleiche, Arithmetik).
Achten Sie auf die Datentypen. Die Typen von Variablen etc. sind erst zur Laufzeit bekannt und müssen dann passen.
Lesen Sie den zu interpretierenden Code aus einer Datei ein.
Testen Sie Ihren Interpreter mit Ihren Beispielprogrammen.
A5.3: Auswirkungen der Grammatik auf den Interpreter (2P)
Sie haben sich vermutlich für eine der beiden Grammatiken (MiniLispA, MiniLispB) entschieden und auf der Basis Ihren Interpreter erstellt.
Welche Auswirkungen hat die Grammatik auf den Interpreter? Machen Sie ein Gedankenexperiment: Überlegen Sie, was Sie alles in Ihrer Implementierung ändern müssten, wenn Sie die jeweils andere Grammatik-Variante nutzen würden.
Bonus: Interaktiver Interpreter (3P)
Bauen Sie eine REPL ein, d.h. geben Sie nach dem Start des Interpreters einen Prompt aus und verarbeiten Sie die Eingaben interaktiv. Wie müssen Sie hier mit der Symboltabelle umgehen?