• Keine Ergebnisse gefunden

Konzeption, Implementierung und Anwendungen eines LISP- Interpreters in Python

N/A
N/A
Protected

Academic year: 2022

Aktie "Konzeption, Implementierung und Anwendungen eines LISP- Interpreters in Python"

Copied!
35
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

Bachelorarbeit

Konzeption, Implementierung und Anwendungen eines LISP- Interpreters

in Python

Eingereicht von:

Sascha Schmalhofer Matrikelnummer 5345197 Abgabedatum 04. Mai 2017

(2)

Inhaltsverzeichnis

Konzeption...2

Sprachdefinition...2

Datentypen...3

Listen...4

Strings...4

Symbol...4

Integer...4

Float...4

Function...4

Gensym...4

File...4

Error...5

Array...5

Eingebaute Funktionen...5

Implemetierung...26

Auswertung eines Ausdruckes...26

Der Reader...27

"unmacro"-Makroexpansion...28

"lispeval"-Ausdrucksauswertung...28

Der Speicher des Interpreters...29

Anwendungen...29

Anwendung 1: Bearbeiten von Bitmap-Dateien...30

Anwendung 2: Einlesen von *.csv-Dateien (Comma-Separated-Values)...32

Anwendung 3: Römische Zahlen...32

Anwendung 4: Parsergeneratoren...33

Anwendung 5: Operatoren für LISP...33

Konzeption

Der Interpreter ist grundsätzlich nicht zu anderen LISP-Interpretern kompatibel. Das heißt, dass mit diesem Interpreter nur LISP-Code ausgeführt werden kann, der speziell für diesen Interpreter geschrieben wurde. Der Interpreter ist auch nicht konform zu irgendeiner Standardisierung.

Sprachdefinition

Die Syntax der Sprache, die vom Interpreter akzeptiert wird, kann formal durch eine Kontextfreie Grammatik in erweiterter Backus-Naur-Form (EBNF) beschrieben werden:

<S> ::= ()|

(<S><A>)|

"<STRING>"|

-<PINT>|

<PINT>|

<STRING>

<PINT> ::= 0<PINT>|1<PINT>|2<PINT>|3<PINT>|

4<PINT>|5<PINT>|6<PINT>|7<PINT>|

8<PINT>|9<PINT>|.<FLOAT>|<EPSILON>

<FLOAT> ::= 0<FLOAT>|1<FLOAT>|2<FLOAT>|3<FLOAT>|

4<FLOAT>|5<FLOAT>|6<FLOAT>|7<FLOAT>|

8<FLOAT>|9<FLOAT>|<EPSILON>

(3)

<A> ::= _<S><A>|<EPSILON>

Wobei <EPSILON> für das leere Wort und <STRING> für eine Zeichenkette mit allen möglichen Zeichen, die im verwendeten Zeichensatz erzeugt werden können, steht.

Funktionen haben grundsätzlich eine fest definierte Anzahl an Parametern und nur einen

Rückgabewert, man beachte aber, dass es sich hierbei sowohl bei den Parametern als auch bei dem Rückgabewert auch um Listen handeln kann, sodass die Anzahl der tatsächlich übergebenen und zurückgegebenen Werte auf diese Weise dynamisch sein kann.

Makros haben nur einen Parameter vom Typ "List". Der Funktionskörper, der beschreibt, wie ein Makro expandiert, hat dies zu berücksichtigen. Der Vorteil dieser Art der Implementierung besteht darin, dass Makros prinzipiell eine variable Anzahl an Parametern haben können. Funktionen haben hingegen in diesem LISP-Dialekt eine fest definierte Anzahl an Parametern.

Datentypen

Der vom Interpreter verstandene LISP-Dialekt hat ein dynamisches Typsystem. Die Typen von Variablen stehen also erst zur Laufzeit fest. Diese dynamischen Variablen werden in einem globalen Wörterbuchstapel verwaltet. Hierzu mehr im Unterpunkt "Implementierung".

Der Interpreter unterstützt die folgende Auswahl an Datentypen:

Typbezeichnung Beschreibung Erzeugender Beispielausdruck

Interne Python-Representierung

List Typ zur

Verarbeitung von Listen bzw.

Bäumen

(1 (2 3 (4 5)) 6 7) "list"-Klasse (durch Python bereitgestellt)

String Typ zur

Verarbeitung von Zeichenketten

"Name" "String"-Klasse

Symbol Bezeichner Name "str"-Klasse (durch Python

bereitgestellt)

Integer Ganzzahltyp mit

(konzeptionell) unendlichem Wertebereich

25 "int"-Klasse (durch Python bereitgestellt)

Float Gleitkommazahl 0.25 "float"-Klasse (durch Python bereitgestellt)

Function Typ für

Funktionale (lambda (x) (* x x)) "Funktion"-Klasse Gensym Typ für Gensyms (gensym <NAME>) "Gensym"-Klasse

File Typ für geöffnete

Dateien

(openfile "...") Python-Klassen für Dateiverwaltung:

_io.TextIOWrapper _io.BufferedReader _io.BufferedWriter Error Typ für Fehler (error "Fehler") "Error"-Klasse

(4)

Array Typ für

Speicherblöcke

(array 10) "bytearray"-Klasse (durch Python bereitgestellt)

Listen

Listen können (theoretisch) beliebig viele Elemente enthalten. Die Elemente einer Liste dürfen dabei unterschiedliche Typen haben. Die Implementierung dieses Datentyps basiert auf den Python- Listen, womit sie den Definition und Restriktionen in [1] unterliegen. Die Funktionen, die auf Listen vorimplementiert sind, werden im Abschnitt "Eingebaute Funktionen" beschrieben.

Strings

Strings repräsentieren Zeichenketten, wobei die Besonderheit dieser Implementierung, auch einzelne Zeichen als Strings zu repräsentieren, zu beachten ist. Auf Strings sind eine ganze Reihe von Funktionen definiert, diese werden auch im Abschnitt "Eingebaute Funktionen" beschrieben.

Sie werden über die in Python eingebauten Strings implementiert, die lediglich in einer neuen Klasse verpackt sind. Eine genauere Beschreibung zum Python-"str"-Datentyp findet sich in [2].

Symbol

Symbol ist der Datentyp von Bezeichnern. Sie werden intern als Python-"str"-Instanz verarbeitet.

Im Unterschied zum "String"-Typ sind sie aber nicht in einer neuen Klasse "verpackt".

Integer

Integer sind in dieser Implementierung der Standard-Datentyp für Wahrheitswerte ("falsch"

entspricht 0, alles Andere zählt als "wahr"), und positive sowie negative Ganzzahlen. Sie basieren auf dem Python-"int"-Datentyp und haben somit einen (theoretisch) unendlichen Wertebereich.

Genaueres hierzu steht in [3].

Float

Bei Float handelt es sich um einen eingebauten Gleitkommatyp. Die Genauigkeit richtet sich nach der Python-Implementierung, auf der der Interpreter läuft. Der intern verwendete Python-"float"- Datentyp wird in [3] beschrieben.

Function

Der Typ Function repräsentiert ein Funktional. Eine Instanz kann mit "lambda" erzeugt werden.

Intern werden diese Funktionale durch eine Instanz der Klasse "Funktion" verwaltet. Der Typ

"Funktion" hat dabei zwei Membervariablen: "Parameter" und "Koerper". "Parameter" beinhaltet die Parameterliste, "Koerper" den Funktionsausdruck.

Gensym

Ein Gensym kann überall dort als Symbol eingesetzt werden, wo ein Bezeichner notwendig ist, der momentan einzigartig ist und dies auch in der Zukunft bleiben wird. Diesen Typ gibt es speziell zur Vermeidung des Variablenüberdeckungsproblemes bei der Programmierung von Makros. In anderen Interpretern, wie beispielsweise CommonLisp wird dieses Problem vom Modulsystem gelöst

(Quelle [4]).

File

(5)

File ist ein Datentyp zur Verwaltung von geöffneten Dateien. Intern werden zur Repräsentation die Python-Typen "_io.TextIOWrapper" für eine Textdatei, "_io.BufferedReader" und

"_io.BufferedWriter" für eine Binärdatei verwendet. Die vorimplementierte Funktion "openfile"

erzeugt diese Typen. Nach dieser Instanzierung kann mit anderen Dateioperationen auf den geöffneten Dateien geschieben oder gelesen werden. Mit "closefile" ist ein Destruktor für diese Instanzen. Generelle Informationen zu den Python Datentypen für Dateioperationen können in [5]

gefunden werden.

Error

Error ist ein Datentyp, mit dem Fehler behandelt werden können. Tritt irgendwo beim Aufruf einer vorimplementierten Funktion ein Fehler auf, so gibt diese eine Instanz dieses Typs zurück. Überall wo ein Error eintsteht wird ein Stacktrace angehängt. Dies gilt aber nicht für Fehler, die nicht in vorimplementierten Funktionen auftreten. Diese Fehler haben keinen Stacktrace und können mit

"error" instanziert werden.

Array

Arrays sind in dieser Implementierung grundsätzlich dynamisch und manipulativ. Letzteres heißt, dass Arrays stets durch die Arrayoperationen "arrayset" und "arrayget" verändert werden, wobei eine Kopie des Arrays immer erst auf ausdrücklichen Wunsch entsteht. Dies unterscheidet die Arraydatenstruktur von allen anderen in der Tabelle aufgelisteten Typen und dient hauptsächlich der Einsparung von Arbeitsspeicherplatz und Rechenzeit. Wird also ein Array einer Funktion

übergeben, handelt es sich dabei tatsächlich nicht um eine Arbeitskopie, sondern um eine Referenz.

Eingebaute Funktionen

Es sind folgende Funktionen im Interpreter vorimplementiert:

SYNTAX:

(and <X> <Y>) KURZBESCHREIBUNG:

"and" dient der logischen Und-Kombination zweier Integer-Werte <X> und <Y>. 0 entspricht dabei einem logischen False, alles Andere einem logischen True.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

SYNTAX:

(or <X> <Y>) KURZBESCHREIBUNG:

"or" dient der logischen Oder-Kombination zweier Integer-Werte <X> und <Y>. 0 entspricht dabei einem logischen False, alles Andere einem logischen True. Falls <X> bereits zu True ausgewertet wurde, wird <Y> nicht mehr ausgewertet.

MÖGLICHE TYPKOMBINATIONEN:

(6)

<X> <Y> Ergebnis

Integer Integer Integer

SYNTAX:

(not <X>)

KURZBESCHREIBUNG:

"not" dient der logischen negation des Ausdruckes <X>.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Integer Integer

SYNTAX:

(& <X> <Y>) KURZBESCHREIBUNG:

"&" dient der Bitweisen Und-Kombination zweier Integer-Werte <X> und <Y>.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

SYNTAX:

(| <X> <Y>) KURZBESCHREIBUNG:

"|" dient der Bitweisen Oder-Kombination zweier Integer-Werte <X> und <Y>.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

SYNTAX:

(~ <X>)

KURZBESCHREIBUNG:

"~" dient der Bitweisen Negation eines Integer-Wertes <X>.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Integer Integer

SYNTAX:

(<< <X> <Y>)

(7)

KURZBESCHREIBUNG:

"<<" schiebt <X> um <Y> bits nach links. Es werden wegen des zugrundeliegenden, (theoretisch) unendlichen Python-Integer-Typs keine Bits abgeschnitten. <Y> muss positiv sein.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

SYNTAX:

(>> <X> <Y>) KURZBESCHREIBUNG:

">>" schiebt <X> um <Y> bits nach rechts. Überflüssige Bits werden abgeschnitten. <Y> muss positiv sein.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

SYNTAX:

(= <X> <Y>) KURZBESCHREIBUNG:

Mit "=" lassen sich zwei Werte <X> und <Y> vergleichen, Falls <X> = <Y> wird 1 zurückgegeben, ansonsten 0.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Symbol/Integer/Float/String/List Symbol/Integer/Float/String/List Integer SYNTAX:

(!= <X> <Y>) KURZBESCHREIBUNG:

Mit "!=" lassen sich zwei Werte <X> und <Y> auf Ungleichheit hin prüfen.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Symbol/Integer/Float/String/List Symbol/Integer/Float/String/List Integer SYNTAX:

(neg <X>)

KURZBESCHREIBUNG:

Mit "neg" lassen sich Integer- und Float-Werte negieren, Strings und Listen umkehren.

(8)

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Integer Integer

Float Float

String String

List List

SYNTAX:

(+ <X> <Y>) KURZBESCHREIBUNG:

Mit "+" lassen sich zwei Werte <X> und <Y> Addieren.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

Float Float Float

Integer Float Float

Float Integer Float

SYNTAX:

(- <X> <Y>) KURZBESCHREIBUNG:

Mit "-" lässt sich Wert <Y> von Wert <X> Subtrahieren.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

Float Float Float

Integer Float Float

Float Integer Float

SYNTAX:

(* <X> <Y>) KURZBESCHREIBUNG:

Mit "*" lassen sich zwei Werte <X> und <Y> Multiplizieren.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

(9)

Float Float Float

Integer Float Float

Float Integer Float

SYNTAX:

(/ <X> <Y>) KURZBESCHREIBUNG:

Mit "/" lässt sich Werte <X> durch <Y> dividieren.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Float

Float Float Float

Integer Float Float

Float Integer Float

SYNTAX:

(> <X> <Y>) KURZBESCHREIBUNG:

">" Gibt 1 zurück, wenn <X> größer ist als <Y> ansonsten 0.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

Float Float Integer

Integer Float Integer

Float Integer Integer

SYNTAX:

(>= <X> <Y>) KURZBESCHREIBUNG:

">=" Gibt 1 zurück, wenn <X> größer oder gleich <Y> ist, ansonsten 0.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

Float Float Integer

Integer Float Integer

Float Integer Integer

(10)

SYNTAX:

(< <X> <Y>) KURZBESCHREIBUNG:

"<" Gibt 1 zurück, wenn <X> kleiner ist als <Y> ansonsten 0.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

Float Float Integer

Integer Float Integer

Float Integer Integer

SYNTAX:

(<= <X> <Y>) KURZBESCHREIBUNG:

"<=" Gibt 1 zurück, wenn <X> kleiner oder gleich <Y> ist, ansonsten 0.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

Float Float Integer

Integer Float Integer

Float Integer Integer

SYNTAX:

(div <X> <Y>) KURZBESCHREIBUNG:

"div" dividiert <X> ganzzahlig durch <Y> . Der Rest wird dabei ignoriert.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

Float Float Integer

Integer Float Integer

Float Integer Integer

SYNTAX:

(mod <X> <Y>) KURZBESCHREIBUNG:

"mod" liefert den Rest einer ganzzahligen Division von <X> durch <Y> .

(11)

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Integer Integer Integer

Float Float Integer

Integer Float Integer

Float Integer Integer

SYNTAX:

(car <X>)

KURZBESCHREIBUNG:

"car" liefert das erste Element einer Liste. Die Bezeichnung ist historisch und stand ursprünglich für

"Content of Adress-Register".

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

List Je nach in Liste verpacktem Typ

SYNTAX:

(cdr <X>)

KURZBESCHREIBUNG:

"cdr" liefert eine Liste mit Ausnahme des ersten Elementes. Die Bezeichnung ist historisch und stand ursprünglich für "Content of Data-Register".

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

List Je nach in Liste verpacktem Typ

SYNTAX:

(cons <X> <Y>) KURZBESCHREIBUNG:

"cons" kopiert die Liste <Y> und fügt dabei ein Listenelement <X> an.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

unbekannt List List

SYNTAX:

(map <X> <Y>)

KURZBESCHREIBUNG:

(12)

"map" kopiert die Liste <Y> und wendet dabei die Funktion <X> auf jedes Element der Liste an.

Hierbei muss die übergebene Funktion genau einen Parameter haben.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Function List List

SYNTAX:

(manipulate <X> <Y>) KURZBESCHREIBUNG:

"manipulate" manipuliert eine Liste <Y>, indem es die Funktion <X> auf jedes Listenelement anwendet. "manipulate" existiert aus Performanzgründen, kommt es darauf nicht an, sollte lieber die Funktion "map" verwendet werden. Auch hier muss <X> einparametrig sein.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Function List 0

SYNTAX:

(if <X> <Y> <Z>) KURZBESCHREIBUNG:

"if" wertet zunächst die Bedingung <X> aus. Wenn <X> zu zu 0 ausgewertet wurde, dann wird <Z>

ausgewertet und zurückgegeben, andernfalls wird <Y> ausgewertet und zurückgegeben.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> <Z> Ergebnis

Integer beliebig beliebig Typ von <Y> oder von <Z>

SYNTAX:

(lambda <X> <Y>) KURZBESCHREIBUNG:

"lambda" ist der Konstruktor eines Funktionals. Es nimmt eine Parameterliste <X> und einen Rückgabeausdruck <Y> und gibt ein Funktional zurück.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

List beliebig Function

SYNTAX:

(let <X> <Y>)

KURZBESCHREIBUNG:

(13)

"let" nimmt genau zwei Parameter, eine Liste mit Definitionen <X> und einen Ausdruck <Y>, in dem diese Definitionen gelten sollen. Nach der Definition eines Listenelementes in der

Definitionsliste <X>, gilt diese Definition auch in der nächsten Definition der Liste bereits. Es ist also beispielsweise gültig zu schreiben:

(let (x 10 y (+ x 2)) (+ x y))

Die Liste der Definitionen muss eine durch zwei Teilbare Länge haben, außerdem muss mit dem ersten Element beginnend jedes zweite Element ein Symbol oder Gensym sein.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

List beliebig Typ von <Y>

SYNTAX:

(def <X> <Y>) KURZBESCHREIBUNG:

"def" legt eine Variable an. Dabei muss <X> ein Symbol und <Y> ein Ausdruck sein, der dem Symbol dann einen Wert gibt. Die Variable wird global definiert.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Symbol beliebig 0

SYNTAX:

(set <X> <Y>) KURZBESCHREIBUNG:

"set" legt eine Variable an. Dabei muss <X> ein Symbol und <Y> ein Ausdruck sein, der dem Symbol dann einen Wert gibt. Die Variable wird lokal definiert.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Symbol beliebig 0

SYNTAX:

(defmacro <X> <Y> <Z>) KURZBESCHREIBUNG:

"defmacro" erzeugt ein Makro. Dabei muss <X> ein Symbol (der Name des Makros) und <Y> ein Symbol für den lokalen Namen der Parameterliste des Makros sein. <Z> ist der Funktionskörper des Makros, der für das Expandieren zuständig ist.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> <Z> Ergebnis

Symbol Symbol beliebig 0

(14)

SYNTAX:

(setreadermacro <X>) KURZBESCHREIBUNG:

"setreadermacro" legt das Readermakro fest. Dabei muss <X> ein Funktional oder der Name eines Funktionals sein.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Symbol 0

SYNTAX:

(read <X>)

KURZBESCHREIBUNG:

"read" ruft den Reader auf. Dabei muss <X> ein String sein.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

String Liste

SYNTAX:

(eval <X>)

KURZBESCHREIBUNG:

"eval" interpretiert die Liste <X>.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Liste beliebig

SYNTAX:

(print <X>) KURZBESCHREIBUNG:

"print" gibt <X> auf der Konsole aus und gibt den Wert von <X> zurück.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

beliebig Typ von <X>

(15)

SYNTAX:

(lisptext <X>) KURZBESCHREIBUNG:

"lisptext" erzeugt einen String, der <X> beschreibt, er enthält genau das, was "print" bei einem Aufruf von (print <X>) auf der Konsole ausgeben würde.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

beliebig String

SYNTAX:

(unmacro <X>) KURZBESCHREIBUNG:

"unmacro" expandiert alle Makros in einer Liste <X>. In anderen LISP-Varianten heißt diese Funktion meistens "macroexpand".

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Liste Liste

SYNTAX:

(unmacro1 <X>) KURZBESCHREIBUNG:

"unmacro1" expandiert alle Makros in einer Liste <X>, es werden allerdings anschließend keine Makros in der Expansion aufgelöst, wie bei "unmacro". Diese Funktion ist sehr nützlich um Makroexpansion zu testen.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

List List

SYNTAX:

(quote <X>) KURZBESCHREIBUNG:

"quote" verhindert die Auswertung des Ausdruckes <X>.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

beliebig Typ von <X>

(16)

SYNTAX:

(openfile <X> <Y>) KURZBESCHREIBUNG:

"openfile" öffnet eine Datei. Im Argument <X> wird der Dateipfad als String übergeben. Das Argument <Y> spezifiziert den Modus, in dem die Datei geöffnet wird. Gültige Werte hierfür sind:

-"r": Lesen einer Textdatei -"w": Schreiben einer Textdatei -"rb": Lesen einer Binärdatei -"wb": Schreiben einer Binärdatei MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

String File

SYNTAX:

(fileread <X> <Y>) KURZBESCHREIBUNG:

"fileread" liest aus einer Binärdatei <X> <Y> Bytes. Im Falle einer Textdatei <X> werden <Y>

Zeichen eingelesen. In beiden Fällen wird nur solange gelesen, bis das Dateiende erreicht ist. Ist das Dateiende erreicht, werden leere Arrays bzw. Leerstrings zurückgegeben. Die Position des

Dateizeigers ändert sich mit jedem Lesevorgang. Um den Dateizeiger neu festzulegen, um von anderer Stelle forlaufend zu lesen kann "fileseek" verwendet werden.

MÖGLICHE TYPKOMBINATIONEN:

<X> <X> Ergebnis

File Integer Array oder String

SYNTAX:

(filereadline <X>) KURZBESCHREIBUNG:

"filereadline" liest ausschließlich aus Textdateien. Aus einer Textdatei <X> wird eine Zeichenkette von der aktuellen Position des Dateizeigers bis zum nächsten Zeichen für das Zeilenende gelesen.

Ist das Ende der Datei erreicht, werden Leerstrings zurückgegeben. Der Dateizeiger kann auch hierbei mit "fileseek" gesetzt werden.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

File String

SYNTAX:

(filewrite <X> <Y>)

(17)

KURZBESCHREIBUNG:

"filewrite" schreibt in eine Binärdatei <X> das Array <Y> . Im Falle einer Textdatei <X> wird der String <Y> geschrieben. Der Dateizeiger kann mit "fileseek" neu festgelegt werden.

MÖGLICHE TYPKOMBINATIONEN:

<X> <X> Ergebnis

File Array oder String 0

SYNTAX:

(fileseek <X> <Y> <Z>) KURZBESCHREIBUNG:

"fileseek" setzt den Dateizeiger der geöffneten Datei <X> an die Position <Y>. <Z> legt fest, auf welche Adressierungsart sich <Y> bezieht. Gültige Werte für <Z> sind:

– 0: Der Anfang der Datei hat den Index 0

– 1: Die aktuelle Position des Dateizeigers hat den Index 0 – 2: Das Dateiende hat den Index 0

"fileseek" gibt die neu festgelegte Position zurück.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> <Z> Ergebnis

File Integer Integer Integer

SYNTAX:

(closefile <X>) KURZBESCHREIBUNG:

"closefile" schließt die geöffnete Datei <X>.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

File 0

SYNTAX:

(readstr)

KURZBESCHREIBUNG:

"readstr" liest eine Zeichenkette vom Benutzer auf der Konsole ein. Die Funktion nimmt keine Parameter.

MÖGLICHE TYPKOMBINATIONEN:

Ergebnis String SYNTAX:

(printstr <X>)

(18)

KURZBESCHREIBUNG:

"printstr" gibt die Zeichenkette <X> auf der Konsole aus. Der Unterschied zu "print" besteht darin, dass "printstr" keine anderen Typen außer String akzeptiert und diese ohne Anführungszeichen ausgibt. Die Funktion gibt <X> zurück.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

String String

SYNTAX:

(get <X> <Y> <Z>) KURZBESCHREIBUNG:

"get" nimmt zwei Indizes <X> und <Y> und kopiert ein Array oder String oder eine Liste <Z> ab dem Index <X> bis zum Index <Y>. Hierbei sind auch negative Indizes gültig, diese werden dann vom Ende des Arrays/des Strings oder der Liste gezählt. Gibt man als Index <X> einen anderen Typ wie Integer an, wird der Index als 0 angenommen. Gibt man als Index <Y> einen anderen Typ wie Integer an, so wird als Index, der höchstmögliche Index angenommen.

Diese Funktion wird über Pythons Slicing implementiert, siehe hierzu auch [6].

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> <Z> Ergebnis

Integer oder beliebig Integer oder beliebig Array oder String oder Liste Typ von <Z>

SYNTAX:

(concat <X> <Y>) KURZBESCHREIBUNG:

"concat" verkettet (kopierend):

• zwei Strings <X> und <Y>

• zwei Arrays <X> und <Y>

• zwei Listen <X> und <Y>

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

String oder Array oder Liste String oder Array oder Liste String oder Array oder Liste

SYNTAX:

(length <X>) KURZBESCHREIBUNG:

"length" gibt die Länge eines Arrays oder eines Strings oder einer Liste zurück.

MÖGLICHE TYPKOMBINATIONEN:

(19)

<X> Ergebnis

String oder Array oder Liste Integer

SYNTAX:

(stringtolist <X>) KURZBESCHREIBUNG:

"stringtolist" erzeugt ausgehend von einem String eine Liste mit den einzelnen Zeichen des Strings als Elemente.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

String Liste

SYNTAX:

(split <X> <Y>) KURZBESCHREIBUNG:

"split" teilt einen String <X> jeweils an einem Substring <Y> auf und speichert die so erhaltenen Teilstrings in einer Liste.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

String String Liste von String

SYNTAX:

(join <X> <Y>) KURZBESCHREIBUNG:

"join" verkettet die Strings einer Liste von Strings <X> mit einem Trenner <Y>.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

Liste von String String String

SYNTAX:

(strpos <X> <Y>) KURZBESCHREIBUNG:

"strpos" sucht nach der Position des Teilstrings <X> im String <Y>. Falls der Teilstring nicht gefunden werden konnte, wird -1 zurückgegeben.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> Ergebnis

(20)

String String Integer

SYNTAX:

(chr <X>)

KURZBESCHREIBUNG:

"chr" gibt zu einem Integer-Wert <X> den zugehörigen Wert in der Zeichentabelle als String der Länge 1 zurück.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Integer String

SYNTAX:

(ord <X>)

KURZBESCHREIBUNG:

"ord" gibt den Index eines Zeichens <X>, repräsentiert als String der Länge 1, als Integer zurück.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

String Integer

SYNTAX:

(array <X>) KURZBESCHREIBUNG:

"array" erzeugt ein dynamisches Array der Länge <X>, die stets in Byte gezählt wird.

Zurückgegeben wird eine Referenz auf dieses Array.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Integer Array

SYNTAX:

(arrayget <X> <Y> <Z>) KURZBESCHREIBUNG:

"arrayget" greift auf ein Arrayelement des Arrays <X> zu. Hierbei werden <Y> Byte an der Stelle

<Z> (in Byte) in einen Integer konvertiert und zurückgegeben. Der Integer wird BigEndian-codiert.

MÖGLICHE TYPKOMBINATIONEN:

(21)

<X> <Y> <Z> Ergebnis

Array Integer Integer Integer

SYNTAX:

(arrayset <X> <Y> <Z> <A>) KURZBESCHREIBUNG:

"arrayset" schreibt ein Arrayelement des Arrays <X>. Es werden <Y> Byte an der Stelle <Z> (in Byte) mit dem Integer <Z> gefüllt. Der Integer wird BigEndian-codiert.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> <Z> <A> Ergebnis

Array Integer Integer Integer Integer

SYNTAX:

(runfile <X>) KURZBESCHREIBUNG:

"runfile" führt die Lisp-Quellcodedatei aus, die durch den Dateipfad <X> spezifiziert wird.

"runfile" gibt immer das Ergebnis der letzten Auswertung im Quellcode <X> zurück.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

String Typ des Rückgabewerts der Datei.

SYNTAX:

(gensym)

KURZBESCHREIBUNG:

"gensym" erzeugt ein neues Gensym und gibt es zurück. Gensyms sind Symbole, die garantiert nur einmal vorkommen. Sie werden benötigt, um das Problem der bereits existierenden Variablen bei der Makroprogrammierung zu lösen.

MÖGLICHE TYPKOMBINATIONEN:

Ergebnis Gensym

SYNTAX:

(block <X>) KURZBESCHREIBUNG:

"block" wertet die Ausdrücke in der Liste von Ausdrücken <X> der Reihenfolge nach aus und hat selbst den Rückgabewert des Letzten Ausdruckes.

(22)

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Liste von LISP-Anweisungen beliebig

SYNTAX:

(do <X> <Y> <Z>) KURZBESCHREIBUNG:

Die "do"- Schleife dient der Iteration. Sie ist sehr komplex und sollte nur verwendet werden, wenn sie nicht durch die Konstrukte "while" oder "until" ersetzt werden kann, da diese schneller

interpretiert werden können. <X> muss eine Liste von Definitionen sein. Sie enthält dreielementige Listen, von denen das erste Element ein Symbol sein muss und eine Schleifenvariable benennt. Das zweite Element ist ein Wert, den die Variable zu Beginn der Iteration haben soll. Das dritte und letzte Element ist ein Ausdruck, der nach einem Durchlauf der Schleife Ausgewertet wird und auf den die Variable dann gesetzt wird. <Y> ist eine Liste von Abbruchbedingungen. Genauer

beschrieben ist ein Element in <Y> ein Ausdruck. Falls dieser Ausdruck zu etwas anderem wie 0 ausgewertet wird, wird die Schleife abgebrochen. <Z> ist ein Ausdruck der als Schleifenkörper dient. Er wird einmal pro Schleifendurchlauf ausgewertet. Nach Ablauf der Schleife hat sie den Rückgabewert, den <Z> bei der letzten Auswertung erzeugt hat. Die "do" Schleife ist eine Kopfgesteuerte Schleife, wird ihr Ausdruck <Z> überhaupt nicht ausgewertet, so wird der Wert 0 als Integer zurückgegeben.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> <Z> Ergebnis

List List beliebig beliebig

SYNTAX:

(repeat <X> <Y> <Z>) KURZBESCHREIBUNG:

Die "repeat"- Schleife dient der Iteration. Sie ist sehr komplex und sollte nur verwendet werden, wenn sie nicht durch die Konstrukte "while" oder "until" ersetzt werden kann, da diese schneller interpretiert werden können. <X> muss eine Liste von Definitionen sein. Sie enthält dreielementige Listen, von denen das erste Element ein Symbol sein muss und eine Schleifenvariable benennt. Das zweite Element ist ein Wert, den die Variable zu Beginn der Iteration haben soll. Das dritte und letzte Element ist ein Ausdruck, der nach einem Durchlauf der Schleife Ausgewertet wird und auf den die Variable dann gesetzt wird. <Y> ist ein Ausdruck der als Schleifenkörper dient. Er wird einmal pro Schleifendurchlauf ausgewertet. <Z> ist eine Liste von Abbruchbedingungen. Genauer beschrieben ist ein Element in <Z> ein Ausdruck. Falls dieser Ausdruck zu etwas anderem wie 0 ausgewertet wird, wird die Schleife abgebrochen. Nach Ablauf der Schleife hat sie den

Rückgabewert, den <Y> bei der letzten Auswertung erzeugt hat. Die "repeat" Schleife ist eine Fußgesteuerte Schleife, das heißt, dass ihr Körperausdruck <Y> mindestens einmal ausgeführt wird, da die Abbruchbedingungen <Z> erst anschließend erstmalig geprüft werden.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> <Z> Ergebnis

(23)

List beliebig List beliebig

SYNTAX:

(while <X> <Y> <Z> <A> <B> <C>) KURZBESCHREIBUNG:

Die "while"- Schleife ist eine Kopfgesteuerte Schleife mit einer Variablen. Die Parameter bedeuten Folgendes:

• <X> Ein Symbol, welches die Zählvariable bezeichnet.

• <Y> Ein Initialwert für die Zählvariable.

• <Z> Ein Ausdruck, der nach einem Schleifendurchlauf ausgewertet und der Zählvariablen zugewiesen wird.

• <A> Ein Ausdruck, der vor jedem Schleifendurchlauf ausgewertet wird. Wird er zu 0 ausgewertet, wird die Schleife abgebrochen.

• <B> ist der Schleifenkörperausdruck, sein Resultat ist auch das Resultat des "while"- Ausdruckes, wenn die Schleife mindestens einmal ausgeführt wurde.

• <C> ist der Ausdruck, der ausgewertet wird und den Resultatwert darstellt, wenn die Schleife abgebrochen wird ohne das der Schleifenkörper <B> ausgewertet wurde.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> <Z> <A> <B> <C> Ergebnis

Symbol Beliebig Beliebig Integer Beliebig Beliebig Beliebig

SYNTAX:

(until <X> <Y> <Z> <A> <B>) KURZBESCHREIBUNG:

Die "until"- Schleife ist eine Fußgesteuerte Schleife mit einer Variablen. Die Parameter bedeuten Folgendes:

• <X> Ein Symbol, welches die Zählvariable bezeichnet.

• <Y> Ein Initialwert für die Zählvariable.

• <Z> Ein Ausdruck, der nach einem Schleifendurchlauf ausgewertet und der Zählvariablen zugewiesen wird.

• <A> Ein Ausdruck, der nach jedem Schleifendurchlauf ausgewertet wird. Wird er zu 0 ausgewertet, wird die Schleife abgebrochen.

• <B> ist der Schleifenkörperausdruck, sein Resultat ist auch das Resultat des "repeat"- Ausdruckes.

MÖGLICHE TYPKOMBINATIONEN:

<X> <Y> <Z> <A> <B> Ergebnis

Symbol Beliebig Beliebig Integer Beliebig Beliebig

SYNTAX:

(loop <X>)

(24)

KURZBESCHREIBUNG:

"loop" ist ein ganz simpler Schleifenausdruck. "loop" wertet <X> solange aus, bis es einen Integer mit wert 0 zurückgibt. Der Rückgabewert ist immer 0.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Integer 0

SYNTAX:

(type <X>)

KURZBESCHREIBUNG:

"type" gibt den Typ von <X> nach der Auswertung von <X> als String zurück. Ausnahme: Typ

"Error".

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Beliebig String

SYNTAX:

(symboltostring <X>) KURZBESCHREIBUNG:

"symboltostring" wandelt ein Symbol <X> in einen String um.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Symbol String

SYNTAX:

(stringtosymbol <X>) KURZBESCHREIBUNG:

"stringtosymbol" wandelt einen String <X> in ein Symbol um.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

String Symbol

SYNTAX:

(iserror <X>)

(25)

KURZBESCHREIBUNG:

Mit "iserror" lässt sich prüfen, ob <X> zu einem Fehler ausgewertet wird. Dies ist notwendig, weil mit "type" nicht der Typ "Error" abgefragt werden kann.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

beliebig Integer

SYNTAX:

(error <X>) KURZBESCHREIBUNG:

Mit "error" lässt sich ein Fehler auslösen. Die Fehlerbeschreibung wird dabei als String <X>

übergeben. Allgemein wird allerdings nur bei vom Interpreter direkt ausgelösten Fehlern ein Stacktrace protokolliert.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

String Error

SYNTAX:

(traceappend <X> <Y>) KURZBESCHREIBUNG:

"traceappend" hängt ein Stacktraceelement <Y> an einen Fehler <X> an. Der Fehler muss vom Interpreter ausgelöst worden sein.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

String Error

SYNTAX:

(dispose <X>) KURZBESCHREIBUNG:

"dispose" löscht ein Symbol <X> aus dem Hauptwörterbuchstapel des Interpreters. Der Rückgabewert ist immer 0.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

Symbol 0

SYNTAX:

(recur <X>)

KURZBESCHREIBUNG:

(26)

Mit "recur" lassen sich Endrekursive Funktionen optimieren. Anstelle des Endrekursiven Aufrufes, lässt sich "recur" schreiben. "recur" ruft die zuletzt aufgerufene Funktion erneut auf, und zwar ohne neuen Stapelspeicherplatz zu benötigen. Es wird die Stackumgebung der zuletzt aufgerufenen Funktion verwendet.

MÖGLICHE TYPKOMBINATIONEN:

<X> Ergebnis

beliebig beliebig

Implemetierung

Der Interpreter wurde in Python 3.3.2 implementiert, nachdem sich eine Implementierung in PASCAL mit der Lazarus IDE als zu aufwendig herausgestellt hat. Python hat demgegenüber den Vorteil, dass das im LISP-Dialekt vorgesehene Typsystem eine Teilmenge des Python-eigenen Typsystems darstellt, was die Programmierung des Interpreters erheblich vereinfacht.

Es werden vom Interpreter zwei Betriebsmodi unterstützt: Zeilenweise Auswertung in einer Read- Eval-Print-Loop (REPL) und Interpretieren von Quellcodedateien, wobei bei letzterem prinzipiell auf gleiche Weise wie in der REPL die einzelnen Zeilen der Quellcodedatei ausgewertet werden.

Eine vollständig ausgewertete Quellcodedatei hat einen Rückgabewert: Das Resultat des letzten Ausdruckes in der Datei. Für die Implementierung der Dateiausführoperationen ist die Python- Funktion "runfile" zuständig. Sie implementiert einige besondere Schreibweisen innerhalb einer Quellcodedatei:

• Mehrere Leerzeichen hintereinander werden als ein Leerzeichen interpretiert.

• Leerzeichen hinter einer sich öffnenden Klammer werden ignoriert.

• Leerzeilen werden wie "nicht vorhanden" behandelt.

• Tabulatoren und das Neue-Zeile-Zeichen werden als ein weiteres Symbol für ein Leerzeichen angenommen.

Auswertung eines Ausdruckes

Die Auswertung eines Ausdruckes innerhalb einer Quellcodedatei unterscheidet sich von der

Auswertung im REPL nur durch die zusätzliche Ausgabe des Resultates bei letzterem. Deshalb wird im Folgenden nurnoch auf die Auswertung eines direkt eingegebenen Ausdruckes eingegangen. Der Ausdruck wird zunächst als Zeichenkette (Python "str"-Klasse) vom Benutzer eingelesen.

Anschließend wird dieser String an den Reader weitergegeben, der Reader-Makros auflöst und daraus eine Python-Liste ("list"-Klasse) macht. Wenn dies erledigt ist, werden in der so

entstandenen Liste durch die Python-Funktion "unmacro" rekursiv alle Makros aufgelöst.

Abschließend wird die so entstandene makrofreie Liste (immernoch "list"-Klasse als Typ) von der Python-Funktion "lispeval" ausgewertet und anschließend ausgegeben. Diese einzelnen Schritte werden im Folgenden noch genauer beschrieben.

Der Reader

Der Reader, der in dieser Implementierung durch die Python-Funktion "read" implementiert wird, löst zunächst das Reader-Makro auf. Definitionsgemäß muss das Readermakro in dieser

Implementierung eine einparametrige Funktion sein, die ausschließlich einen String als Parameter nimmt und auch nur einen String zurückgeben darf. Diese Abbildung wird zuerst ausgeführt.

Standardmäßig ist das Readermakro auf die Identitätsfunktion gesetzt. Im restlichen Teil ist "read"

als Parser für die bereits im Punkt "Sprachdefinition" erwähnte KFG (hier noch einmal aufgeführt)

(27)

zu sehen:

<S> ::= ()|

(<S><A>)|

"<STRING>"|

-<PINT>|

<PINT>|

<STRING>

<PINT> ::= 0<PINT>|1<PINT>|2<PINT>|3<PINT>|

4<PINT>|5<PINT>|6<PINT>|7<PINT>|

8<PINT>|9<PINT>|.<FLOAT>|<EPSILON>

<FLOAT> ::= 0<FLOAT>|1<FLOAT>|2<FLOAT>|3<FLOAT>|

4<FLOAT>|5<FLOAT>|6<FLOAT>|7<FLOAT>|

8<FLOAT>|9<FLOAT>|<EPSILON>

<A> ::= _<S><A>|<EPSILON>

Daraus ergibt sich für "read" folgendes Verhalten:

"read" prüft nach der Übersetzung durch das Readermakro zunächst, ob der ihm übergebene String mit einer öffnenden Klammer beginnt und mit einer schließenden Klammer endet. Sollte dies der Fall sein, wird, je nachdem ob der String ein Symbol für die leere Liste "()" ist (Länge 2), entweder die leere Liste (Typ: Python "list"-Objekt) zurückgegeben oder eine Liste mit Elementen, die durch Aufteilen des Strings zwischen den Klammern an den Leerzeichen mit "LispSplit" und

anschließender Anwendung von read auf diese Teilstrings entstanden ist. An dieser Stelle noch etwas zur Funktion "LispSplit": "LispSplit" teilt einen String bei Leerzeichen auf, die sich nicht innerhalb einer Klammerung (Gänsefüsschen und normale Klammern) befinden. Die nächste Überprüfung, die "read" vornimmt, bezieht sich auf die dritte Ableitung von <S> in der obigen KFG. Hier wird anhand führender und abschließender Gänsefüsschen erkannt, ob "read" eine Zeichenkette verarbeiten soll. Ist dies der Fall, so muss lediglich eine Instanz der Klasse "String"

mit der Zeichenkette zwischen den Gänsefüsschen zurückgegeben werden. Andernfalls probiert

"read" aus, ob es sich bei dem String, den "read" übergeben bekommt, um einen Integer oder Float handeln könnte. Dieser Vorgang lässt sich als Automat modellieren:

Im Automat lässt sich erkennen, dass der String für einen Integer oder Float entweder mit einer Ziffer oder einem Minuszeichen beginnen darf. Dieses wird gefolgt von einer oder mehrerer Ziffern. Sollte irgendwann ein Dezimalpunkt folgen, so landet die Erkennung im Zustand 3. Hier können dann weitere Ziffern folgen. "read" erkennt dann anhand des finalen Zustandes, ob ein Integer oder ein Float erkannt wurde und konvertiert dann diese Strings entsprechend. Im Automat sind nur akzeptierende Pfade eingetragen. Sollte also an irgendeiner Stelle ein Zeichen auftreten, Abbildung 1: Automat zur Erkennung von Integer und Float

(28)

welches nicht an einer weiterführenden Kante steht, so wird der gesamte Vorgang abgebrochen und der "read" übergebene String als Symbol angenommen.

Abschließend ist noch zu bemerken, dass dieser Parser keine Fehler generieren kann, es gibt also keine ungültigen Worte in der Sprache.

"unmacro"-Makroexpansion

"unmacro" ist die Funktion, die Makros ausrollt. In anderen LISP Umgebungen heißt diese Funktion meistens "macroexpand". Die Funktion ist im Interpretercode als Python-Funktion implementiert, hier nimmt sie ein Python-List-Objekt, welches den LISP-Code mit Makros repräsentiert. "unmacro" läuft rekursiv über die ihr übergebene Liste und darin enthaltene Liste u.s.w. und verschont dabei nur Listen, die mit dem Symbol "quote" anfangen, davor, dass jeweils ihr erstes Element auf Vorhandensein in der Makrotabelle (hier als Python-Dictionary realisiert) geprüft wird. Sollte dies der Fall sein, wird das identifizierte Makro unter seinem Namen in der Symboltabelle eingetragen. Außerdem wird der Parameter, den das Makro bekommt in der

Symboltabelle vermerkt. Dann wird der Makrocode ausgeführt und das Resultat anstelle der Liste geschrieben, die mit dem Namen eines Makros begann.

Abschließend werden noch in diesem Resultat die Makros auf gleiche Weise aufgelöst. Dieser Punkt ist das Einzige, was "unmacro1" von "unmacro" unterscheidet: In "unmacro1" werden im Resultat einer Makroexpansion keine Makros mehr aufgelöst. Dies macht die Funktion "unmacro1"

sehr nützlich, wenn es darum geht, Makroexpansionen von makrogenerierenden Makros zu

debuggen. In Common Lisp (erstmals beschrieben von Guy Steele in 1984 (Quelle: [7] im Vorwort auf Seite ix) gibt es im Gegensatz zu dieser Implementierung bereits über einhundert vordefinierte Makros (Quelle: [8] Seite 93). Es ist leicht vorstellbar, dass unter diesen Bedingungen eine

vollständige Makrexpansion von einem Menschen kaum noch zu verstehen ist.

"lispeval"-Ausdrucksauswertung

Die Python-Funktion "lispeval" ist das Kernstück des Interpreters. Hier werden alle Funktionen implementiert, die der Interpreter unterstützt. Die allesumgebende Endlosschleife kann zunächst ignoriert werden, sie ist für die Implementierung des "recur"-Statements notwendig. Zuerst wird festgelegt, dass eine leere Liste nicht weiter ausgewertet wird. Dann wird je nach übergebenem Typ weiter verzweigt. String,Integer,Float oder Error-Werte werden nicht weiter ausgewertet, ein

Symbol wird in der Symboltabelle (dem Wörterbuchstapel SYMBOL) nachgeschlagen. Falls es sich bei dem übergebenen Wert um eine Liste handelt, codiert das erste Element die Funktion, die

aufgerufen werden soll, die restlichen Elemente werden als Parameter interpretiert. Die Statements werten in der Regel ihre Parameter von links nach rechts aus und verarbeiten die so entstandenen Werte anschließend. Hiervon gibt es allerdings auch Ausnahmen. "or" beispielsweise wertet zunächst nur den linken Parameter aus. Wird dieser bereits zu 1 ausgewertet, wird der rechte Wert garnicht ausgewertet. Eine weitere zwingende Ausnahme hiervon stellt "if" dar. "if" wertet

selbstverständlich immer nur einen seiner beiden Zweige (2. und 3. Parameter) aus, je nachdem, was bei der vorhergegangenen Auswertung des 1. Parameters herauskam.

Es ist auch möglich, den Namen einer Funktion berechnen zu lassen, da auch eine Liste an erster Stelle einer Liste stehen darf und diese dann ausgewertet wird. Hier kann auch ein "lambda"- Ausdruck stehen. Handelt es sich bei einem ersten Element einer zu interpretierenden Liste nicht um den Namen einer vorimplementierten Funktion oder um eine Liste, wird in der Symboltabelle nach diesem Namen gesucht. Findet sich der Name in der Symboltabelle, werden zunächst die Parameteranzahlen abgeglichen, anschließend werden die übergebenen Parameter ausgewertet.

Nachdem das erledigt ist, wird ein neuer Kontext für die Funktion erzeugt, indem ein neues Wörterbuch auf den Wörterbuchstapel aufgelegt, und mit den Parameter-Wert-Paaren gefüllt wird.

Nach der Auswertung des Funktionskörpers wird das Wörterbuch wieder vom Wörterbuchstapel genommen und das Ergebnis zurückgegeben.

(29)

Abschließend soll noch kurz die Funktionsweise des "recur"-Statements erläutert werden:

"recur" macht es dem Programierer möglich, den Interpreter auf eine Endrekursion hinzuweisen.

Diese Endrekursion wird dann vom Interpreter optimiert. Der Interpreter ist nicht in der Lage, von sich aus Endrekursionen zu erkennen. "recur" wird vom Programmierer anstelle des rekursive Aufrufes geschrieben. Der Interpreter merkt sich stets auf einem Stapel, welche Funktion als letztes aufgerufen wurde. Kommt jetzt ein "recur"-Statement, weis der Interpreter, welche Funktion erneut ausgewertet werden soll. Diese Funktion ruft er jetzt auf, und zwar ohne ein neues Wörterbuch auf den Wörterbuchstapel zu legen und ohne neuen Platz auf dem Stapel zu verbrauchen, den sich Python und LISP teilen. Für letzteres ist die bereits am Anfang dieses Abschnittes erwähnte

Endlosschleife verantwortlich: "recur" legt den Parameter der "lispeval"-Funktion l erneut fest und springt mithilfe dieser Schleife an den Anfang der "lispeval"-Funktion. Dabei wird kein neuer Stapelspeicherplatz alloziert und die Auswertung der letztaufgerufenen Funktion kann erneut in ihrem Kontext ablaufen, allerdings mit anderen Parametern.

Der Speicher des Interpreters

In diesem Abschnitt soll noch einmal genauer auf den globalen Speicher des Interpreters eingegangen werden.

Der Interpreter merkt sich zu jedem Zeitpunkt:

• Die Symboltabelle: Ein Wörterbuchstapel mit allen Symbolen, die definiert wurden. Der Wörterbuchstapel ist die zentrale Datenstruktur des Interpreters. Sie ist als eine Liste von Python-Dictionarys implementiert. Wird ein Symbol bzw. Wort nachgeschlagen, werden zu erst die obenaufliegenden Wörterbücher durchsucht, später erst die Wörterbücher weiter unten. Dies setzt das Prinzip "lokal definierte Symbole vor global definierten Symbolen"

durch.

• Die Makrotabelle: Ein einfaches Python-Dictionary, in dem alle Makros abgespeichert sind.

• Das Readermakro: Eine Funktion, die einen String auf einen String abbildet.

• Der Recurstack.

• Der Gensymzaehler (im Modul Types.py): Ein Zaehler, um einen Namen zu vergeben, der einmalig ist. Dieser Zähler kann vom Programierer nicht direkt beeinflusst werden.

Anwendungen

Im Folgenden sollen einige Anwendungen beschrieben werden, die mit dem Interpreter realisiert wurden. Sie dienen unter anderem als erweiterter Test für den Interpreter.

Vorab aber noch einige Anleitungen zur Praxis:

Es gibt prinzipiell zwei Anwendungsmöglichkeiten des Interpreters, er kann entweder Zeilenweise Ausdrücke in einer "Read-Eval-Print-Loop" Auswerten und das Ergebnis ausgeben, oder eine ganze LISP-Quellcodedatei interpretieren. Für beide Varianten muss zunächst der Interpreter gestartet werden. Der Interpreter wurde unter Windows 7 mit Python 3.3.2 in der Entwickungsumgebung IDLE (Version 3.3.2) entwickelt. In dieser Umgebung wird der Interpreter wie folgt gestartet.

1. Öffnen der Entwicklungsumgebung mit der Datei Lisp.py

2. Starten des Interpreters durch drücken der F5-Taste oder alternativ durch Klick auf "Run"

und dann auf "Run Module".

Nun können LISP-Ausdrücke eingegeben und ausgewertet werden. Das Resultat wird dann jeweils angezeigt. Hier kann jetzt auch mit ":run "+Dateipfad der Befehl gegeben werden, eine ganze Datei zu interpretieren. Alternativ kann auch die LISP interne Anweisung "runfile" verwendet werden, die in Quellcodedateien zum starten anderer Quellcodedateien benutzt werden muss.

Quellcodedateien können mit einem gewöhnlichen Texteditor erstellt werden, komfortabler ist aber das an dieser Stelle empfohlene Notepad++. Notpad++ bietet von sich aus als einstellbare

Programmiersprache LISP. Dies kann im Menüpunkt "Sprachen" eingestellt werden. Hierbei

(30)

werden allerdings Schlüsselworte, die Teilweise nicht im Interpreter implementiert sind, als solche markiert und teilweise auch umgekehrt.

Anwendung 1: Bearbeiten von Bitmap-Dateien

Der Interpreter unterstützt einlesen und schreiben von Binärdateien über die Funktionen "openfile",

"fileread" und "filewrite". Um diese Funktionen auch hinsichtlich auf ihre Performanz zu testen sollen hier zwei 24-Bit-Windows Bitmaps eingelesen, miteinander kombiniert und anschließend abgespeichert werden. Das Programm schreibt in eine der beiden Bitmaps die jeweils andere als Wasserzeichen ein.

Um Bitmaps lesen zu können benötigt man zunächst eine Schnittstelle zu Windows-Bitmaps.

Dieses Format wurde aufgrund seines relativ einfachen Aufbaus gewählt und wird durch die hier vorgestellte Schnittstelle nicht vollständig unterstützt. Die Schnittstelle unterstützt lediglich unkomprimierte 24-Bit-Bitmaps. 24 Bit heißt hier, dass für einen jeden Pixel 24 Bit Speicherplatz bereitgestellt werden. Das sind acht Bit für den Blau-Wert, acht Bit für den Rot-Wert und acht Bit für den Grün-Wert. Genauere Informationen zu Windows-Bitmaps können in den Dokumentationen zum Windows Application Programmer Interface (WinAPI) gefunden werden. Ich habe meine Informationen aus der Win32 SDK Reference Help, die auch online gefunden werden kann.

Letztlich ist eine Windows-Bitmapdatei wie folgt aufgebaut:

Die Datei beginnt mit dem BITMAPFILEHEADER, dieser wird vom BITMAPINFOHEADER gefolgt, sodass sich folgender Aufbau ergibt, wobei Bezeichnungen, die mit "bf" beginnen zum BITMAPFILEHEADER und Bezeichnungen, die mit "bi" beginnen, zum BITMAPINFOHEADER gehören:

Position in

Byte Bezeichnung API-Typ Typlaenge in

Byte

0 bfType WORD 2

2 bfSize DWORD 4

6 bfReserved1 WORD 2

8 bfReserved2 WORD 2

10 bfOffBits DWORD 4

14 biSize DWORD 4

18 biWidth LONG 4

22 biHeight LONG 4

26 biPlanes WORD 2

28 biBitCount WORD 2

30 biCompression DWORD 4

34 biSizeImage DWORD 4

38 biXPelsPerMeter LONG 4

42 biYPelsPerMeter LONG 4

46 biClrUsed DWORD 4

50 biClrImportant DWORD 4

Generell ist zu beachten, dass unter Windows in der Regel LittleEndian-codiert wird, also mit dem höchstwertigen Byte an letzter Position.

(31)

Nun kann man mittels den erwähnten Dateioperationen und Arrays eine Schnittstelle zu diesen Bitmaps erstellen, die Quellcodedatei

".\Anwendungen\BitmapsBearbeiten\Bitmap24BitInterface.lisp" macht genau das. Sie stellt zur Arbeit mit Bitmap-Dateien zwei Funktionen zur Verfügung: "ReadBitmap" und "WriteBitmap".

"ReadBitmap" bekommt einen Dateipfad und gibt eine Liste zurück, die

• die Breite der Bitmap in Pixeln

• die Höhe der Bitmap in Pixeln

• die eigentlichen Daten der Bitmap als Array

in dieser Reihenfolge enthaelt. Die Bitmap wird hierbei als Array in den Arbeitsspeicher geladen und steht nun zur Bearbeitung bereit. Die Pixel sind von links nach rechts Zeilenweise von unten nach oben abgespeichert, wobei es noch eine kleine Unschönheit gibt, die aus Performancegründen nicht von der Bitmap-Schnittstelle übernommen werden kann: Es gibt eventuell bedeutungslose Bytes in dem Array. Bei 24-Bit-Windows Bitmaps werden die Zeilen mit bedeutungslosen Bytes aufgefüllt, sodass die Anzahl der Bytes pro Zeile durch vier teilbar ist. Es gilt also für die Anzahl n der Bytes pro Zeile:

n=⌊3w+3 4 ⌋∗4 oder in LISP-Notation:

(def n (* 4 (div (+ 3 (* 3 w)) 4)))

Legt man an der Bitmap ein übliches Koordinatensystem mit y-Achse von unten nach oben und x- Achse von links nach rechts, so kann man mit Koordinaten x und y und der Anzahl an Bytes pro Zeile n mit folgender Formel die Position i des Pixels im Array ermitteln:

i=y∗n+x

In der Quellcodedatei ".\Anwendungen\BitmapsBearbeiten\Wasserzeichen.lisp" wird in ein Bitmap- Bild ein Wasserzeichen eingebracht. Anhand einer Bitmap, die ein Wasserzeichen vorgibt, wird in eine weitere Bitmap das Wasserzeichen geschrieben und in einer neuen Datei abgespeichert. Hierbei sollte die wasserzeichenerhaltende Bitmap breiter und höher wie die wasserzeichengebende Bitmap sein. Das Wasserzeichen wird in der Mitte der Wasserzeichenerhaltenden Bitmap positioniert. Wenn ein Pixel in der wasserzeichenbeschreibenden Bitmap absolut schwarz ist (rot = 0, gruen = 0, blau = 0), dann werden alle Farbanteile um den Wert 70 erhöht. Ist dies nicht möglich, wird der Farbwert auf 255 gesetzt. Ist ein Pixel nicht absolut schwarz, behält das Bild dort seine Orginalfarbe.

Anwendung 2: Einlesen von *.csv-Dateien (Comma-Separated-Values)

Comma-Separated-Values ist genau wie XML eine Möglichkeit, Textbasiert Informationen in Dateien abzuspeichern. Diese Dateiformate kommen immer mehr in Mode, da die Lesbarkeit durch einen Menschen gegenüber der schnellen lesbarkeit durch einen Computer immer mehr in den Vordergrund rückt. Comma-Separated-Values hat allerdings gegenüber XML den Nachteil, dass keine baumartigen Strukturen abgespeichert werden können, sondern ausschließlich

zweidimensionale Tabellen. Hierzu werden die Zeilen einer Tabelle durch das Neue-Zeile-Zeichen (unter Windows zwei Zeichen) und die Zellen durch ein Komma oder Semikolon oder Leerzeichen voneinander abgetrennt. Es ist klar, dass etwas derartiges mit dem Interpreter sehr leicht über die eingebauten Textdateifunktionen "openfile", "filereadline" und "filewrite" und die Stringfunktionen

"split" und "join" realisiert werden kann.

Die Quellcodedatei ".\Anwendungen\CommaSeparatedValues.lisp" stellt die Schnittstelle zu Comma-Separated-Values dar. Sie enthält eine Funktion "ReadCSV", die einen Dateipfad zu einer CSV-Datei erhält, diese dann einliest und als Liste von Liste von String zurückgibt. Die Funktion

(32)

"WriteCSV", die ebenfalls in dieser Quellcodedatei implementiert ist, bekommt einen Dateipfad und eine Liste von Liste von String und schreibt daraus eine CSV-Datei.

Anwendung 3: Römische Zahlen

Römische Zahlen sind bei Nummerierungen und Jahreszahlen immernoch recht beliebt. Hier werden sie als Beispiel für die implementierung eines Parsers für eine reguläre Sprache

herangezogen. Es gab und gibt sehr viele verschiedene Schreibweisen und Regeln, die nicht immer konsequent beachtet wurden und werden, hier wird die folgende Definition angewandt. Zahlen, die nicht der Definition entsprechen, werden als "illegal" behandelt.

Definition:

Zahlzeichen M D C L X V I

Wert 1000 500 100 50 10 5 1

Regeln:

• Die Zahlzeichen werden bei der Darstellung einer Zahl der Größe nach sortiert von links (groß) nach rechts (klein) notiert, Ausnahme ist die Subtraktionsregel.

• Die Zahlzeichen D, L und V dürfen nicht aufeinander folgend im Zahlstring vorkommen

• Die Zahlzeichen C, X und I dürfen maximal drei mal aufeinander folgend im Zahlstring vorkommen.

• Subtraktionsregel: Es darf ein I oder X oder C genau einmal vor einem jeweils größeren Zahlzeichen stehen, was bedeutet, dass der Wert dieses Zahlzeichens dann vom Wert des folgenden Zahlzeichens abzuziehen ist.

Fasst man die römischen Zahlen nach dieser Definition nun als formale Sprache auf, so kann man sie als kontextfreie Sprache in EBNF aufschreiben. Eine Modellierung als Automat wäre ebenfalls möglich, da die Sprache regulär ist. Dieser Automat ist aber sehr groß und unhandlich, wenn man ihn mit der Grammatik vergleicht.

Römische Zahlen als Kontextfreie Grammatik in EBNF:

<S> ::= M*<D>|M*IM<D>|M*XM<D>|M*CM<D>

<D> ::= D<C>|ID<C>|XD<C>|CD<C>|<C>

<C> ::= C<L>|CC<L>|CCC<L>|

IC<L>|CIC<L>|CCIC<L>|CCCIC|

XC<L>|CXC<L>|CCXC<L>|CCCXC|<L>

<L> ::= L<X>|

IL<X>|

XL<X>|<X>

<X> ::= X<V>|XX<V>|XXX<V>|

IX<V>|XIX<V>|XXIX<V>|XXXIX|<V>

<V> ::= I|II|III|IV|V|VI|VII|VIII|<EPSILON>

Grundsätzlich werden die römischen Zahlen in LISP hier als Zeichenkette dargestellt. Aus der Kontextfreien Grammatik kann man nun direkt den LISP Quellcode für den Parser schreiben.

Hierzu betrachte man sich die Funktion "RomToInt" aus der Datei "RoemischeZahlen.lisp", die eine römische Zahl in einen Integer konvertiert. Der Trick hierbei ist, dass die Stufe einer Ableitung in

(33)

der Grammatik entspricht. In einer Stufe, bzw. Einem Zweig des "cond"-Statements werden die jeweiligen Ableitungsregeln der Stufe umgesetzt.

Die implementierung der römischen Zahlen wäre nur unvollständig, wenn nicht auch Integer in römische Zahlen konvertiert werden könnten. Hierfür ist die Funktion "IntToRom" aus der Quellcodedatei ".\Anwendungen\RoemischeZahlen.lisp" zuständig. Zu beachten ist hierbei, dass nur positive Integer konvertiert werden können. Die Funktion zieht stets rekursiv den größtmöglich einsetzbaren römischen Zahlwert vom zu konvertierenden Wert ab und hängt das Zahlzeichen an den bereits konvertierten Zahlstring an. Dies Funktioniert allerdings nur mit einem Trick:

Zweielementige Zeichenfolgen in Subtraktionsschreibweise werden als ein Zeichen angesehen und auf diese Weise passend eingefügt.

Anwendung 4: Parsergeneratoren

Um in LISP einen Parser angenehm programmieren zu können, sollte auf Parsergeneratoren nicht verzichtet werden. In LISP ergibt sich im Bezug auf Parsergeneratoren eine Besonderheit: LISP benötigt keine Externen Parsergeneratoren.

Während Parsergeneratoren für andere Programmiersprachen, wie beispielsweise YACC oder BISON externe Programme sind, denen man eine Eingabe gibt, woraus sie einen lauffähigen Parser für eine Programmiersprache generieren, ist dies in LISP nicht zwingend notwendig. Durch die Makroprogrammierbarkeit von LISP, ergibt sich die Möglichkeit den Parsergenerator komplett als Makro zu realisieren. Das sieht dann so aus: Im LISP Quelltext finden sich Makroaufrufe, die als die Eingabe für den Parsergenerator zu betrachten sind. Nach der Makroexpansion wurde diese Stelle im Quelltext durch einen lauffähigen Parser ersetzt. Von dieser Möglichkeit wird im Folgenden Gebrauch gemacht.

Anwendung 5: Operatoren für LISP

LISP unterstützt von sich aus keine Operatoren, obwohl sie Quellcode zweifelsfrei lesbarer machen.

Wenn im Zusammenhang mit LISP von Operatoren die Rede ist, ist zu beachten, dass in der Literatur zu LISP teilweise auch Funktionen als Operatoren bezeichnet werden. In dieser Arbeit sind allerdings richtige Operatoren, die eine eigene Syntax benötigen, gemeint.

In dieser Anwendung soll gezeigt werden, wie man mithilfe der LISP-eigenen Reader-Makros Operatoren zur Syntax hinzufügen kann oder generell wie Reader-Makros verwendet werden können, um die Syntax von LISP zu verändern.

Der Einfachheit halber werden hier nur ein- und zweistellige Operatoren implementiert. Um Konflikte mit dem Standard-LISP zu vermeiden, werden Berechnungen entweder komplett in Standard-LISP-Notation oder in der Operatorsprache notiert. Die Operatorsprache wird durch Mengenklammern kenntlich gemacht. Mathematische Klammerung zuerstauszuwertender Ausdrücke wird in OPL durch die eckigen Klammern dargestellt, da die Einfachen Klammern in LISP bereits den Listen vorbehalten sind. Die Operatorsprache wird dann vom Reader durch ausrollen des Reader-Makros übersetzt.

Die Syntax der Sprache wird um die folgenden Ableitungen erweitert:

<S> ::= "<STRING>"|

(<S>( <S>)*)|

(|-)(0|..|9)(0|..|9)*|

(|-)(0|..|9)(0|..|9)*.(0|..|9)(0|..|9)*|

{<OPL>}

<OPL> ::= <S><OP><S>|<OP><S>|[<S>]

(34)

Wobei <OPL> die Operatorsprache ist, desweiteren ist <OP> ein Operator. In der zweiten

Ableitung wird die Operatorsprache genauer definiert: Ein- und Zweistellige Operatoren beliebiger Länge (Länge: hier die Anzahl an Zeichen, die für einen Operator geschrieben werden).

Zwischen Operator und Operand darf kein Leerzeichen vorkommen, ein Teil der Operatoren enthält zwangsweise Leerzeichen, wie beispielsweise " and ".

Implementiert werden die folgenden Operatoren (Sortiert nach Bindungsstärke):

Prinzipiell muss LISP keine Teilsprache der nach der Installation des Reader-Makros vom Interpreter verstandenen Sprache sein. Steht eine vom Interpreter ausführbare LISP-Funktion XtoLisp mit nur einem Aufrufparameter vom Typ String zur Verfügung, die Sprache X

(repräsentiert als Zeichenkette) nach LISP (repräsentiert als Zeichenkette) übersetzt , so kann durch das Statement (setreadermacro XtoLisp) veranlasst werden, dass fortan X-Anweisungen

interpretiert werden. LISP bzw. der Interpreter kann somit theoretisch die Gestalt jeder Programmiersprache annehmen!

Literaturverzeichnis

[1] Python v3.3.2 documentation, Version: 15. Mai 2013, Abschnitt 4.6.4: "Lists"

[2] Python v3.3.2 documentation, Version: 15. Mai 2013, Abschnitt 4.7: "Text Sequence Type-str"

[3] Python v3.3.2 documentation, Version: 15. Mai 2013, Abschnitt 4.4: "Numeric Types - int,float,complex"

[4] Paul Graham: On Lisp, Kapitel 9.6 "Avoiding Capture with Gensyms", Seite 128

[5] Python v3.3.2 documentation, Version: 15. Mai 2013, Abschnitt 16.2 "io-Core tools for working

Operator LISP-Funktion Assoziativität Bindungsstärke

|| or links 0

&& and links 1

| | links 2

& & links 3

= = links 4

!= != links 4

> > links 4

< < links 4

>= >= links 4

<= <= links 4

shl << links 5

shr >> links 5

: cons links 5

++ Concat links 5

+ + links 6

- - links 6

* * links 7

/ / links 7

div div links 7

mod mod links 7

§ car rechts 8

$ cdr rechts 8

! not rechts 8

~ ~ rechts 8

' quote rechts 8

(35)

with streams"

[6] Python v3.3.2 documentation, Version: 15. Mai 2013, Abschnitt 4.6.1: "Common Sequence Operations"

[7] Paul Graham: On Lisp, im Vorwort, Seite ix

[8] Paul Graham: On Lisp, Abschnitt 7.4: "Testing Macroexpansion", Seite 93

Referenzen