Definieren

Viele Arbeitsweisen von Forth sind bis hierher schon vorgestellt worden. Eine Eigenschaft von Forth, und mit Sicherheit nicht ihre unwichtigste, ist allerdings noch nicht erwähnt worden. Bevor sie aber erklärt werden kann, sind einige Vorbemerkungen erforderlich.

Das erste, was deutlich gemacht werden muss, ist die extreme Einfachheit des Forth-Kompilers. Kompiler anderer Sprachen sind recht umfangreiche Programme, denen es gelingt sehr viele Konstruktionen in einer Hochsprache in Maschinensprache zu übersetzen. Im Gegensatz dazu besteht der Forth-Kompiler aus einer, noch dazu sehr kleinen, Definition. Alle Konstuktionen, wie IF ... ELSE ... THEN oder DO ... LOOP werden nicht vom Kompiler bereit gestellt, sondern sind in diese Worte ausgelagert.

Die zweite Sache, die angesprochen werden muss ist, dass Wörter in Forth zwei unterschiedliche Verhalten haben können, die Laufzeitverhalten und Kompilierzeitverhalten genannt werden. Das ist nun etwas schwierig und sollte etwas eingehender erklärt werden. Am einfachsten geht das an einem Beispiel. Ich nehme dafür das Wort VARIABLE. Wie bekannt ist, kann mit diesem Wort eine Variable angelegt werden, aber das dabei zwei Verhalten verknüpft sind, ist bisher noch nicht thematisiert worden. Schauen wir uns dazu einmal die Verwendung dieses Wortes an:

  VARIABLE foo
    ok
  5 foo !
    ok

Im ersten Fall wurde die Variable foo angelegt, sie wurde in das Wörterbuch kompiliert. Was dabei passiert, nennt man das Kompilierzeitverhalten von VARIABLE. Dabei passiert folgendes:

  1. Es wird ein neuer Eintrag im Wörterbuch geschaffen, der den Namen 'foo' trägt.
  2. In diesen Eintrag wird eingetragen, was dieser neue Eintrag machen soll, wenn er aufgerufen wird. Da dieses Verhalten nur in VARIABLE stehen kann und ausgeführt wird, wenn die Variable 'läuft', nennt man dies das Laufzeitverhalten.
  3. Hinter diesem Eintrag wird Platz bereitgestellt, um den Inhalt der Variablen aufnehmen zu können.

Im zweiten Fall wird also das Laufzeitverhalten der Variablen foo ausgenützt; die Adresse des Speicherplatzes wird auf den Stack gelegt.

Es gibt zwei Arten von Wörtern in Forth, die sowohl ein Laufzeit- als auch ein Kompilierzeitverhalten haben: Die Definitionswörter und die Kompilerwörter. Ich beginne mit den einfacherern.

Definitionswörter

Es sind schon einige Definitionsworte vorgestellt worden: VARIABLE, CONSTANT aber auch CREATE. Was ist diesen Worten gemeinsam? Nun, ihnen ist gemeinsam, dass sie alle Wörter im Wörterbuch erzeugen, die ein ähnliches oder sogar gleiches Verhalten haben.

Das Wort VARIABLE erzeugt immer ein Wort im Wörterbuch, das beim Aufruf die Adresse seines Parameterbereichs auf den Stack legt. Analog legen alle Wörter, die mit CONSTANT erzeugt wurden, den Inhalt ihres Parameterbereichs auf den Stack. Bei CREATE ist es eigentlich genau so, wie bei Variable. Ein mit CREATE erzeugtes Wort, legt die Adresse seines Parameterbereichs auf den Stack, wenn es aufgerufen wird. Der Unterschied zu VARIABLE besteht eigentlich nur darin, dass mit CREATE erzeugte Worte dort keinen Platz reservieren. Die Definition von VARIABLE ist daher übrigens ausgesprochen einfach:

  : VARIABLE CREATE 2 ALLOT ;

Man sieht, dass VARIABLE indirekt CREATE verwendet. Da VARIABLE den gleichen Laufzeitkode wie CREATE hat, braucht kein eigener Laufzeitkode festgelegt zu werden. Will man ein solches Verhalten allerdings angeben, dann braucht man: DOES>

Das Vorgehen ist dabei eigentlich wieder recht einfach:

: DEFINITION CREATE (Kompilzeittätigkeit) DOES> (Laufzeittätigkeit) ;

Am einfachsten macht man sich das einmal an einem einfachen Beispiel klar. Die Definition von CONSTANT lautet vereinfacht:

  : CONSTANT CREATE , DOES> @ ;

Nun kann man sich ansehen, was passiert, wenn man CONSTANT aufruft und was passiert, wenn man ein damit erzeugtes Wort aufruft. Stellen sie sich vor, sie definieren:

  314159 CONSTANT PI

Genau so einfach kann man nun auch eigene Definitionswörter erstellen. Fangen wir mit etwas einfachem an, einem eindimensionalen Array von Bytes, wobei die Anzahl der Bytes beim Aufruf angegeben werden soll.

  : BYTES CREATE DUP , ALLOT DOES> 2+ + ;

Wenn wir nun ein neues Wort erzeugen, passiert folgendes:

  10 BYTES fritz
    ok

Es wird ein Bytefeld 'fritz' angelegt, das Platz für 10 Bytes enthält. Vor diesem Platz wird abgelegt, wieviel Platz vorhanden ist. Will man auf eines dieser Bytes zugreifen, dann reicht:

  3 fritz C@

Was passiert: Das einkompilierte DOES> legt zur Laufzeit die Adresse des Parameterbereichs auf den Stack, dazu werden erst mal 2 addiert, um die Zelle zu überspringen, in der die Länge des zur Verfügung stehenden Platzes gespeichert ist. Damit hat man die Adresse des 1. Bytes. Dann werden 3 addiert, was die Adresse des vierten Bytes bedeutet. Mit C@ wird der dortige Wert ausgelesen.

Nun kommt der eigentliche Vorteil: Man kann das Laufzeitverhalten aller mit BYTES definierten Worte sofort ändern, wenn man die Definition von BYTES ändert! Angenommen, man ändert die Definition folgendermaßen:

  : BYTES CREATE DUP , ALLOT
    DOES> 2DUP @ U< NOT ABORT" Falscher Index" 2+ + ;

Nun holt man den gewünschten Index und den zur Verfügung stehenden Platz auf den Stack und prüft, ob der Index noch zum Platz passt. Ist das nicht der Fall, wird eine Fehlermeldung ausgegeben und ansonsten wie gewohnt die gewünschte Adresse zurück gegeben.