Forth kennt von sich aus keine Datentypen, allerdings ist es in Forth möglich jeden Datentyp zu erzeugen. Manch einer sieht das als Nachteil andere sehen darin einen der größten Vorteile von Forth. Der Hintergrund dafür, dass das möglich ist, liegt darin, dass man in Forth direkten Zugriff auf den Speicher des Computers hat; man kann also mit ihm machen 'was man will'.
Es gibt zwar einige Worte, mit denen man Variablen anlegen kann, aber der eigentliche 'Witz' bei der Sache liegt darin, dass man in Forth den Speicher des Computers selber auslesen und beschreiben kann, wie man will; aber der Reihe nach:
Zwei Worte kennen sie schon: VARIABLE
und
ALLOT
. Trotzdem soll hier noch einmal an ihre
Arbeitsweise erinnert werden.
VARIABLE
schafft eine benannte Variable. Genauer
gesagt, wird dem Forth-Vokabular ein neuer Name hinzugefügt, und
dieser Name dient dazu eine einfache Zahl zu speichern und wieder
lesen zu können.
VARIABLE fritz ok
Schfft zum Beispiel eine Variable mit dem Namen 'fritz'. Wie kann
man in dieser Variablen nun Werte speichern, von ihr lesen oder sie
anzeigen lassen. Dazu gibt es drei Worte, die ihnen zum Teil auch
schon bekannt sein dürften. Hier soll aber auch erklärt werden, wie
sie funktionieren. Das erste, was dabei erklärt werden muss ist die,
wie fritz
denn nun arbeitet? Eigentlich ist es ganz
einfach, fritz
legt einfach eine Adresse auf den Stack
und zwar nicht irgendeine Adresse, sondern die Adresse, bei der
Zahlen gespeichert werden können.
fritz ok . 134545136 ok
Bei ihnen wird die Zahl mit ziemlicher Sicherheit eine andere sein,
aber hier bedeutet es, dass an der Adresse 134545136 Platz für eine
Zahl geschaffen wurde und dieser Platz mit dem Namen 'fritz'
verknüpft ist. Wie speichert man nun eine Zahl an dieser Adresse?
Dazu gibt es ein weiteres Forth-Wort: !
(genannt:
"Store"). Dieses Wort bekommt einen Wert und eine Adresse übergeben
und speichert diesen Wert an der Adresse ab. Da nun
fritz
eine Adresse auf dem Stack ablegt, ist das
Speichern eines Wertes in fritz
sehr einfach:
5 fritz ! ok
Das nächste Wort, dass nun besprochen werden soll, ist das Wort
@
(genannt: "Fetch"). Dieses Wort bekommt eine Adresse
übergeben und liest den Wert, der an dieser Adresse gespeichert ist,
aus und legt ihn auf den Stack. Da hier wieder das Gleiche gilt, wie
beim Speichern, ist das Auslesen einer Variablen genau so einfach:
fritz @ . 5 ok
Mitunter will man den Inhalt einer Variablen einfach nur ausgeben
und nicht weiter damit arbeiten. In diesem Fall ist es einfacher
einen weiteren Befehl zu nutzen: ?
. Er stellt eine
Kombination aus @
und .
dar.
fritz ? 5 ok
Was ist nun, wenn man mehr als eine Zahl speichern will? Es ist ja
zum Beispiel denkbar, dass man eine doppelte Zahl speichern will,
die natürlich bei dem von VARIABLE
bereitgestellten
Platz nicht hinpasst. Es gibt in manchen Forth-Systemen das Wort
2VARIABLE
, aber eigentlich ist es unnötig, denn es gibt
das Wort ALLOT
, dass sie auch schon kennen.
ALLOT
verschiebt das Ende des Forth-Wortschatzes um
soviele Byte nach hinten, wie ihm als Argument übergeben werden. Da
nun eine neue Variable immer am Ende dieses Wortschatzes angehängt
wird, entsteht dadurch mehr Platz.
Bauen wir uns doch eine Variable, die Platz für eine doppelte Zahl schafft. Dazu muss man allerdings wissen, wieviele Bits eine einfache Zahl in seinem System einnimmt. In lina sind das 32 Bits, also 4 Byte. Dann legen wir mal los:
VARIABLE cremer 4 ALLOT
VARIABLE
schafft wieder ein neues Wort, hier 'cremer',
und sorgt dafür, dass Platz für eine einfache Zahl dort angelegt
wird. Mit dem 4 ALLOT
wird dafür gesorgt, dass noch
vier weitere Bytes Platz geschaffen wird, also insgesamt genug für
eine doppelte Zahl. Auch dass sollte man mal ausprobieren.
2.0 cremer 2! ok
Das dabei verwendete Wort 2!
sollte selbsterklärend
sein. Nun können wir mit cremer
wieder genau so
arbeiten, wie wir es gerade mit fritz
gelernt haben.
Man achte nur darauf, dass es sich nun um eine doppelte Zahl
handelt.
cremer 2@ ok D. 20 ok
Da nun klar ist, wie man zum Beispiel Platz für eine doppelte Zahl schafft, sollte auch klar sein, wie man noch mehr Platz schafft. Trotzdem auch hier noch ein Beispiel: Stellen sie sich vor, sie brauchen Platz für 5 (einfache) Zahlen. Das ist nun einfach:
VARIABLE 5ZAHL 16 ALLOT ok
Stellvertretend für andere Positionen soll hier nun verdeutlicht werden, wie man auf die vierte Zahl zugreifen kann. Die Sache ist sehr einfach, wenn man sich erinnert, dass eine Variable beim Aufruf die Adresse auf den Stack legt, an der eine Zahl gespeichert werden kann und wenn man außerdem wieß wie lang eine Zahl beim eigenen Forth-System ist (zur Erinnerung: Bei mir sind es 32 Bit = 4 Byte).
Ruft man 5ZAHL
auf, dann erhält man die Adresse der
ersten Zahl. Addiert man zu dieser Adresse 4 hinzu, dann erhält man
die Adresse der zweiten Zahl. Abermals vier addiert gibt dann die
Adresse der dritten Zahl. Es ist klar, dass man 12 addieren muss, um
an die Stelle der vierten Zahl zu kommen. Wenn man dort etwas
speichern will, ist das nun ganz einfach:
5ZAHL 12 + ! ok
Und genau so einfach kann man es auch wieder auslesen:
5ZAHL 12 + @ ok
Ich denke, dass das Prinzip verstanden ist. Es geht aber noch
besser! Nun kommt ein neues Wort ins Spiel, dass in Forth eine
wichtige Bedeutung hat: CREATE
Forth kennt 'von Haus aus' eine Menge Worte und sie wissen auch, wie
man dieser Liste von Worten noch weitere hinzufügt. Ohne hier in die
Details zu gehen, kann ich ihnen schon mal verraten, dass all diese
Worte in einer Liste abgelegt sind. Mit CREATE
erzeugen
sie ein neues Wort und hängen es an die Liste an. Wenn sie also
eingeben:
CREATE NEUESWORT
Dann wird ein neues Wort in die Liste eingetragen und kann von dem Moment an genau so verwendet werden, wie alle anderen Worte. Genau wie eine Variable legt es beim Aufruf seine Adresse auf den Stack. Der Unterschied zu einer Variablen liegt darin, dass dort kein Platz reserviert ist -- versuchen sie bitte nicht dort etwas zu speichern.
Aber man kann etwas besseres, wenn man das Wort ,
hinzu
nimmt. Das Komma bekommt einen Wert übergeben und speichert ihn am
Ende der Wortliste, dem Dictionary ab. Außerdem wird das Ende der
Liste entsprechend erhöht, so dass der Eintrag eines weiteren Wortes
diese Werte nicht mehr überschreibt.
Hätte man statt des Obigen, das folgende eingegeben:
CREATE NEUESWORT 10 , 11 , 21 , 22 , 50 ,
Dann hätte man nicht nur ein neues Wort geschaffen, das wie eine Variable funktioniert, sondern man hätte auch Platz für 5 Zahlwerte geschaffen und diese sogar schon mit Werten belegt. Probieren sie es aus:
NEUESWORT 12 + @ . 22 ok
Sie sehen, es funktioniert. Auf diese Art kann man also nicht nur Variablen erzeugen, die mehr als eine Zahl aufnehmen können, sondern die sogar schon mit Werten belegen. In vielen Situationen ist das sinnvoll.
Neben den Variablen für einfache und doppelte Zahlen, kann man in
Forth allerdings auch auf kleinere Einheiten zugreifen, auf Bytes.
Das ist sinnvoll, weil zum Beispiel Zeichenketten oft aus Bytes
aufgebaut sind. Die zugehörigen Wörter heißen dann auch
C!
und C@
, die genau wie !
und @
arbeiten, allerdings immer nur ein Byte lesen
oder Schreiben. Der Buchstabe 'C' deutete dabei schon darauf hin,
wie sie einzusetzen sind, denn 'C' ist eine Abkürzung für
'Character', also (Buchstaben)Zeichen.
Eine kleine Ergänzung gibt es noch, wenn man in Forth von Speicher und Variablen spricht: Die Konstanten.
Es ist in Forth (und nicht nur da) guter Programmierstil Werte, die sich ändern können, in Konstanten zu verpacken. Ein sehr gutes Beispiel ist die Größe in Byte, die bei einer aktuellen Forth-Variante eine einfache Zahl einnimmt. Verwendet man hierfür eine Konstante, dann wird Programmkode viel portabler und der Grund dafür wird sofort klar, aber zunächst soll einmal die Frage geklärt werden, wie in Forth Konstanten definiert werden. Wenn man sich an die Forth-Sprechweise gewöhnt hat, eigentlich ganz einfach:
5 CONSTANT foo
Mit der obigen Zeile wird eine Konstante 'foo' erzeugt und dieser Konstanten wird der Wert '5' zugewiesen; so einfach ist das.
Was ist nun der Unterschied zwischen einer Konstanten und einer Variablen? Der erste Unterschied liegt darin, dass man einer Konstanten keinen neuen Wert zuweisen kann (oder nur mit Schwierigkeiten). Außerdem 'arbeitet' eine Konstante auch anders als eine Variable. Bei einer Variablen erhielt man beim Aufruf eine Adresse zurück von der dann der Wert ausgelesen werden konnte, oder eben ein neuer Wert eingetragen werden konnte. Das ist bei einer Konstanten nicht so. Hier erhält man sofort den Wert der Konstanten.
foo . 5 ok
Worin liegt der Vorteil von Konstanten. Nun, nehmen wir das Beispiel von vorhin. Wir hatten geschrieben:
NEUESWORT 12 + @
Was passiert, wenn man diese Wortdefinition auf einem 'alten' Forth
aufruft, bei dem eine einfache Zahl nicht 32 sondern nur 16 Bit lang
ist? Ganz einfach: Bei einem derartigen Forth beanspruchen die 5
Werte, die NEUESWORT
enthält nur 5 mal 2, also 10 Byte.
Mit 12 +
erreichen wir also einen Speicherbereich, der
nicht nur nicht mehr im Berech von NEUESWORT
liegt,
sondern darüber hinaus an einer Stelle, von der wir gar nicht
wissen, was dort liegt. Solange wir da nur Werte auslesen wird unser
Programm nicht mehr laufen. Noch kritischer wird es, wen wir
versuchen dort einen neuen Wert abzulegen. Ein Systemabsturz kann
die Folge sein.
Was wir eigentlich vorhatten, war ja, auf das vierte Element von
NEUESWORT
zuzugreifen. Wäre nun eine Konstante
definiert, die angibt, wieviele Byte eine einfache Zahl beansprucht,
sagen wir mal: CELL
, dann hätten wir besser
geschrieben:
NEUESWORT CELL 3 * + @
Überträgt man das auf ein anderes Forth-System, dann können zwei
Dinge passieren. Entweder es gibt das Wort CELL
dort
auch, dann brauchen wir uns nicht darum zu kömmern, wie lang eine
Zelle in diesem System ist, die Konstante regelt das für uns. Oder
aber, das Wort CELL
existiert nicht. Nun, in dem Fall
meckert Forth die obige Eingabe an und wir wissen auch, was wir zu
tun haben.
Man sollte also immer dann mit Konstanten statt direkten Zahlen arbeiten, wenn man sicher stellen will, dass Programme möglichst portablel sein sollen. Außerdem haben Konstanten den Vorteil, dass sie Programme auch lesbarer machen.
Der Umgang mit Speicher ist in Forth sicherlich etwas ungewöhnlich, insbesondere dann, wenn man von einer 'modernen' Programmiersprache her kommt. Gerade Programmiersprachen wie C# verbergen gerade den Aspekt der Speicherverwaltung vor dem Programmierer. Meist existiert ein Befehl wie 'new', mit dem eine neue Instanz einer Klasse ins Leben gerufen wird und die Hintergründe, wie die dann entstandenen Objekte miteinander verbunden werden und wo im Speicher sie überhaupt liegen, werden für den Programmierer nicht transparent.
Es gibt sehr gute Gründe dafür, dass sich die modernen Programmiersprachen gerade in dieser Richtung entwickelt haben. Man kann ziemlich viel Unsinn anstellen, wenn man auf den Speicher direkten Zugriff hat. Versuchen sie mal ein:
5 0 !
Wenn sie ein 'gutes' Forth haben, erhalten sie eine Fehlermeldung. Ein 'normales' Forth wird einfach sang- und klanglos abstürzen.
Man sollte also vermeiden, 'einfach so' im Speicher herumzuschreiben
oder von dort zu lesen. Trotzdem wäre es ja ab und an mal ganz
interessant zu sehen, was im Speicher so passiert. Manche
Forth-Varianten bieten dazu das Wort DUMP
, es ist aber
nicht in allen Forth-Varianten verfügbar und daher soll es hier kurz
vorgestellt werden und dann auch einmal programmiert werden. Es ist
sehr nützlich.
Was ist dieses Wort DUMP
und was macht es? Nun, wie der
Name schon sagt, soll es 'dumpen'. Das ist ein ziemlich alter
Begriff und er besagt, dass man sich den Speicher und seine Inhalte
direkt ansehen will. Dazu hat man sich schon sehr früh auf ein
bestimmtes Format geeinigt (Was mit früh gemeint ist und wie
man mit Hexdumps umgeht, liest man am besten hier.)
Wie sieht ein solcher Hexdump aus, und wie wird er gelesen? Nun von Interesse ist natürlich die Adresse des Speichers, die man gerade sieht, dann der Inhalt. Dieser sollte einmal in 'Zahlform' und einmal in 'Zeichenform' vorliegen. Der Grund dafür ist, dass manchmal eben eher die Zeichen interessieren und manchmal eher die Zahlen.
Wie ich oben schon dargelegt habe ist es oft einfacher sich die Bytes in hexadezimaler Darstellung anzusehen, da so ein Byte immer in zwei Zeichen passt; darum heißen die Dumps auch 'Hex'dump. Aber uach für die Zeichen gibt es ein Überlegung. Der ASCII-Zeichensatz besteht aus druckbaren und nicht druckbaren Zeichen und alle Zeichen des ASCII-Zeichensatzes sind in 7 Bit kodiert, also den Werte nvon 0 bis 127. Ein Byte kann aber auch größere Werte bis 255 darstellen. Was macht man mit denen?
Hier soll nun Schritt für Schritt ein DUMP
-Wort
vorgestellt werden. Das ist vor allem für diejenigen gedacht, die
kein solches Wort haben, aber auch als Übung für die Progrmmierung
in Forth.
Der erste Schritt besteht darin, dass man nachdenkt. Unser Wort
DUMP
soll eine Adresse übergeben bekommen und dann eine
Zahl, die angibt, wieviele Zeilen a 16 Byte von der Adresse an
ausgegeben werden sollen. Es ist daher sinnvoll sich zunächst mal
Gedanken über eine Dumpzeile zu machen.
Eine Dumpzeile soll zu Beginn die Adresse ausgeben, dann den Speicherinhalt ab da in Zahlenform und schließlich in Zeichenform. Da danach eventuell noch eine Zeile ausgegeben werden soll, sollte der Befehl, der eine Zeile ausgibt eine Adresse bekommen und auch eine Adresse ausgeben und zwar die, mit der dann sofort die nächste Zeile ausgegeben werden kann.
Der erste Teil der Zeile soll die Adresse ausgeben. Am besten wäre es, wenn dieses Wort die Anfangsadresse auch wieder zurück gibt, da dann damit sofort auch die Zahlenform ausgegeben werden kann. Ein solches Wort zu schreiben ist einfach:
: DUMP-ADR ( addr -- addr ) HEX DUP 0 <# # # # # # # # # #> TYPE ." | " DECIMAL ;
Ich denke große Teile des Wortes sind selbsterklärend. Nachdem auf
die hexadezimale Zahlenbasis umgeschaltet wurde, wird die Adresse
verdoppelt, damit man sie nach Ablauf des Wortes noch zur Verfügung
hat und dann eine 0 dazu geschrieben, da die Wörter <#
,
#
und #>
eine doppelte Zahl erwarten und
Adressen einfache Zahlen sind.
Dann werden 8 Stellen der Adresse ausgegeben. Das liegt daran, dass mein Forth ein 32 Bit Forth ist. 32 Bit sind 4 Byte, von denen jedes zwei Stellen benötigt. Bei anderen Forthsystemen sollte man das anpassen. Hinter der Adressausgabe kommt noch ein Trennzeichen, um die Adresse von der Zahldarstellung zu trennen.
Nun gehts an die Zahldarstellung. Aus Gründen, die in einem späteren Kapitel noch ausführlicher besprochen werden, ist es gute Forth-Praxis möglichst kleine Worte zu schreiben. Daher sollte es erst mal ein Wort geben, das ein Byte ausgibt. Gefolgt werden soll es von einem Leerzeichen und die Adresse soll, um Eins erhöht, weiter gegeben werden, damit sofort das nächste Byte ausgegeben werden kann. Auch das ist kein Problem:
: 1DUMPBYTE ( addr -- addr ) HEX DUP C@ 0 <# # # #> TYPE SPACE 1+ DECIMAL ;
Hier sieht man auch, wie das Wort C@
mal im Einsatz
gebraucht wird.
Nun sollen vier Bytes hintereinander ausgegeben werden und danach ein zusätzliches Leerzeichen, um die ganze Sache ein bisschenstrukturierter zu machen.
: 4DUMPBYTE ( addr -- addr ) 1DUMPBYTE 1DUMPBYTE 1DUMPBYTE 1DUMPBYTE SPACE ;
Nun können alle 16 Bytes ausgegeben werden. Danach sollte wieder die Anfangsadresse liegen.
: 16DUMPBYTE DUP 4DUMPBYTE 4DUMPBYTE 4DUMPBYTE 4DUMPBYTE DROP ;
Das letzte DROP
dient dazu die erhöhte Adresse zu
'vergessen'. Auf dem Stack liegt die Anfangsadresse für die
Zeichenausgabe.
Ich will die Sache einfach halten. Mit einem 127 AND
kann man die unteren sieben Bit 'maskieren', das oberste Bit wird
dadurch einfach gelöscht. Damit werden zwar Bytes, die gar kein
Zeichen darstellen sollen auch wieder zu ASCII-Zeichen, aber das
stört nicht. Kritischer ist ein anderes Problem. ASCII-Zeichen, die
kleiner als 32 sind und das Zeichen mit der Nummer 127 sind nicht
darstellbar. Für diese Zeichen soll ein Punkt ausgegeben werden und
ansonsten das Zeichen selber. Die Zeichenausgabe soll wieder eine
Adresse übergeben bekommen und die um eins erhöhte Adresse zurück
geben.
: DUMPZEICHEN (addr -- addr ) DUP C@ 127 AND DUP 32 < SWAP DUP 127 = ROT OR IF 46 EMIT DROP ELSE EMIT THEN 1+ ;
Damit können nun 16 Zeichen und dann ein Zeilenvorschub ausgegeben werden.
: 16DUMPZEICHEN (addr -- addr ) 16 0 DO DUMPZEICHEN LOOP CR ;
Nun haben wir alles zusammen, was eine Dumpzeile ausmacht und brauchen es nur noch zu einem Wort zusammen zu setzen:
: DUMPZEILE ( addr -- addr ) DUMP-ADR 16DUMPBYTE 16DUMPZEICHEN ;
Und schon ist das DUMP
-Wort fertig:
: DUMP ( addr n -- ) CR 0 DO DUMPZEILE LOOP DROP ;
Spielen wir mal mit unserem neuen Wort ein bisschen rum. Sie
erinnern sich, dass wir eine Konstante 'foo' definiert hatten. Mal
sehen, ob wir sie wieder finden. Das Wort '
, das dazu
verwendet wird, wird auch später noch erklärt. Hier verwenden wir es
einfach mal:
' foo 5 DUMP B71CAAE0 | A1 BB 04 08 00 00 00 00 05 00 00 00 FF 8B FF 82 !;.............. B71CAAF0 | FC 00 3F 0B BF F8 BF 2F C2 FC 2F F8 2F FF FC 2F |.?.?x?/B|/x/.|/ B71CAB00 | C2 FC 2F FF FF FF FF CB F7 8B F7 8B FF CB FE 2B B|/....Kw.w..K~+ B71CAB10 | F2 FE 2F F8 BF 82 FF 01 FC 2F F0 BF C3 0B FF F7 r~/x?...|/p?C..w B71CAB20 | 0B FF 0B BC 2E F0 BB BC 2F F8 BF FF C2 F0 BF F0 ...<.p;<.x?.Bp?p ok
Tja, das sieht schon ziemlich interessant aus, auch wenn die Adressen an der linken Seite bei ihnen sicherlich andere sein dürften. Aber wo ist unser 'foo'? Nun wir sind dahinter gelandet. Wenn man den Aufruf etwas ändert, dann sieht das Bild folgendermaßen aus:
' foo 16 - 5 DUMP B71CAAD0 | 05 00 00 00 B8 AA 1C B7 03 00 00 80 66 6F 6F 20 ....8*.7....foo B71CAAE0 | A1 BB 04 08 00 00 00 00 05 00 00 00 FF 8B FF 82 !;.............. B71CAAF0 | FC 00 3F 0B BF F8 BF 2F C2 FC 2F F8 2F FF FC 2F |.?.?x?/B|/x/.|/ B71CAB00 | C2 FC 2F FF FF FF FF CB F7 8B F7 8B FF CB FE 2B B|/....Kw.w..K~+ B71CAB10 | F2 FE 2F F8 BF 82 FF 01 FC 2F F0 BF C3 0B FF F7 r~/x?...|/p?C..w ok
Nun sieht die Sache anders aus. Das 'foo' taucht auf und zwar in der ersten Zeile ganz rechts. Allerdings steht noch ein Leerzeichen dahinter (hexadezimal = 20). Dann folgt die Zeile, die wir schon kennen. Und da taucht auch in der Mitte, beim 9ten Byte die '5' auf, für die unsere Konstante steht.
Auch wenn sie hier sicher noch nicht verstehen können, was die ganzen anderen Bytes bedeuten, vertrauen sie mir, das kommt noch und ich denke, dass sie schon mal einen kleinen Überblick bekommen haben, wie man mit dem Speicher in Forth umgeht.