ATOS - Around The Operating System Das ATOS-Magazin 6/96

# Forth-Kurs, Teil 2

Im ersten Teil des Kurses verschafften wir uns einen groben Überblick über die Möglichkeiten von FORTH:

- Rechnen in FORTH

- Worttypen

- Stackmanipulationen

- Logische Grundoperationen

- Vergleichsoperatoren

- Entscheidungen

Dieser zweite Teil beschäftigt sich vornehmlich mit Ausgabe-Funktionen, Stringbehandlung, Schleifen sowie Datenstrukturen. Vorher folgt aber noch ein Beispiel für die Leistungsfähigkeit von FORTH sowie eine leider notwendige Fehlerkorrektur zur FORTH-Version, die der ATOS beigepackt war.

Übersicht:
Ein Beispiel für die Leistungsfähigkeit von FORTH
Fehlerbereinigung
Tips zum Start
Einblick in das Innere von Forth
Der Returnstack
Ein- und Ausgabe von Zahlen
Änderung der Zahlenbasis
Uuml;bung 1
Lösungen 1
Zahlen in Strings umwandeln
Übung 2
Lösungen 2
Eingabe von Zahlen
Strings in Zahlen wandeln
Schleife do .. loop
FORTH und Datenstrukturen
Strings
Anwendung

### Ein Beispiel für die Leistungsfähigkeit von FORTH

Beim Flughafenprojekt in Saudi-Arabien Mitte der achtziger Jahre hat die FORTH Inc., California mit 30 Programmierjahren in 18 Monaten Software entwickelt, bei der ein früheres Team (C) in drei Kalenderjahren mit 100 Programmierjahren gescheitert war.

### Fehlerbereinigung

... es kam, wie es kommen mußte!

Bei der Korrektur von mFORTH (Zeitnot) wurde im appl_init und rsrc_gaddr Aufruf (Vocabular gem) ein Register vertauscht (A3 statt A2). Das führt beim Aufruf leider zu einem Absturz mit 2 Bomben). Ein paar andere Kleinigkeiten konnten bei der fälligen Korrektur ebenfalls beseitigt werden.

Alle Anwender, die mehr als das, was im Kurs angeboten wurde, ausprobiert oder ein Demofile mit AES-Aufrufen geladen haben, bitte ich hiermit um Entschuldigung!

Im öffentlichen Programmteil der Maus LB finden Sie eine korrigierte Version von mForth (ForthXXX.zip).

Fehler im Listing der Turtle-Grafik:

statt: bload vt52.bin >voc vt52

muß es heißen: bload bin\vt52.bin >voc vt52

Wird die Default Forth.ini Datei geladen, so kommt es zum Absturz, wenn kein Cookie installiert ist. Entfernen Sie die folgenden Zeilen:

    bios also
    s" NVDI" get_cookie
    #if   cr .( NVDI Version: ) hex
          @ w@++ <# # # ascii . hold # #> drop type
          .(  Date: )
          @ <# # # # # ascii . hold # # ascii . hold # # #> drop type
          decimal
    #else cr .( NVDI not installed)
    #endif

    s" Gnva" get_cookie
    #if   cr .( Geneva cookie is set to: ) @ . #endif

Ein Fehler in der Routine: pfa>nfa führte dazu, daß ein make auf einem Rechner mit 68000er nicht funktionierte.

Die Version 1.0 wird hoffentlich alle Systemaufrufe dokumentiert haben - und das sind einige hundert...

### Tips zum Start

Wenn Sie Forth.tos als Applikation anmelden (*.4th), so lädt mForth automatisch das ausgewählte File.

Sie dürfen NIE! GEM-Aufrufe tätigen, wenn Sie Forth als TOS-Programm gestartet haben. Geben Sie in diesem Fall forth.tos den Namen forth.prg.

In Multitaskingsystemen ist es nicht notwendig, daß Sie forth.tos umbenennen, da beim ersten appl_init-Aufruf aus Forth heraus die Aufrufe als eigenständige Tasks vom Betriebssystem behandelt werden. Bei VDI-Aufrufen ist dies nicht nötig.

### Einblick in das Innere von Forth

Wenden wir uns den FORTH-Worten zu, die für Forthsysteme typisch sind. Wir unterteilen das System in einen inneren und äußeren Interpreter. QUIT repräsentiert den äußeren Interpreter. Um eine Vorstellung über die Wirkungsweise zu erhalten, hier die vereinfachte Definition von quit (quit ist in mForth unsichtbar)

    : quit ( -- )
       r0 @ rp !    /* Returnstackpointer setzen */
       state off    /* Interpretmodus */
       begin        /* Quit ist eine Endlosschleife */
          .status   /* Status ausgeben */
          query     /* Auf eine Eingabe warten .. */
          interpret /* .. und diese interpretieren */
       repeat ;

In quit kommt interpret natürlich eine besondere Bedeutung zu. Um diese zu würdigen, widmen wir uns diesem im nächsten Teil des Kurses etwas ausführlicher.

Sie sehen, das Forthsystem ist relativ einfach aufgebaut. Einiges muß "von Hand" erledigt werden; das hat allerdings den Vorteil, daß Ihnen der Compiler bei weitem nicht so viele Vorschriften macht wie C oder BASIC. Vektorisiert man z.B. Interpret, so kann man auch eigene Interpret-Routinen installieren.

WORD | Stack | Beschreibung
r0 | ( - adr ) | Variable: Startadr des Returnstacks
rp | ( - adr ) | Variable: Adr des Returnstackpointers
query | -- | Holt Zeichen vom Eingabegerät
state | -- adr | Variable, Inhalt:
|  | 0 = Interpretermodus
|  | 1 = Compilermodus

### Der Returnstack

Wie Sie bereits aus Teil 1 wissen, hat Forth zwei Stacks, nämlich den Datenstack und den Returnstack. Auf dem Returnstack liegt die Rücksprungadresse des zuletzt aufgerufenen Wortes. Wenn der Returnstack innerhalb eines Wortes nicht verändert wird, kann der Returnstack als Zwischenspeicher "mißbraucht" werden. Der Returnstack verhält sich hier wie if ... endif; zu jedem >r gehört ein r>.

WORD | Stack | Returnstack | Beschreibung
>r | n -- | -- n | bringt n auf den Returnstack
r> | -- n | n -- | holt n vom Returnstack
r@ | -- n | n -- | Kopie des obersten Zahl vom Returnstack

Besondere Beachtung verdient der Aufruf, von >r und r> in einer do .. (+)loop Schleife!

### Ein- und Ausgabe von Zahlen

Zwei Worte zur Zahlenausgabe sind Ihnen bereits bekannt: . und .s Durch die Erweiterbarkeit von FORTH können wir die Ausgabe in weiten Bereichen selbst beeinflussen.

Weitere vordefinierte Worte zur Zahlenausgabe

WORD | Stack | Beschreibung
u. | n -- | Zahl ohne Vorzeichen ausgeben
.r | n i -- | Zahl mit i Stellen rechtsbündig ausgeben

Testen Sie: -1 u. <cr> und 100 12 .r <cr>

### Änderung der Zahlenbasis

Nach dem Programmstart befindet sich FORTH im Dezimal-Modus (angezeigt durch & im Prompt), d.h. das alle Zahlen als Dezimalzahlen interpretiert werden. Auch die Ausgabe erfolgt dezimal.

In FORTH gibt es eine Variable namens base. Der Inhalt von base stellt die gerade gültige Zahlenbasis dar. In mForth sind bereits definiert:

WORD | Stack | Beschreibung
decimal | -- | Zahlenbasis Dezimal (10 base !)
hex | -- | Zahlenbasis Hexadezimal (16 base !)
bin | -- | Zahlenbasis Binär (2 base !)

Den Inhalt von base bekommt man auf den Stack mit: base @ . <cr> Es kann natürlich auch eine andere Zahlenbasis gewählt werden.

z.B.: decimal 3 base ! <cr>

Es sind jetzt folgende Zahlen gültig: 0 1 und 2

Experimentieren Sie ein wenig mit einer anderen Zahlenbasis.

### Übung 1

Wählen Sie ein beliebiges Zahlensystem und schauen sich jeweils den Inhalt von base mit base @ . <cr> an. Wundern Sie sich warum sie immer 10 erhalten?

1. Schreiben Sie ein Wort, das Ihnen die aktuelle Zahlenbasis dezimal anzeigt, ohne die gerade aktuelle zu verändern.

2. Schreiben Sie ein Wort, das Ihnen die oberste Zahl hexadezimal ausgibt. Die aktuelle Zahlenbasis soll nicht verändert werden.

### Lösungen 1

1. : .base ( - ) base @ dup decimal . base ! ;

2. : .hex ( n - ) base @ swap hex . base ! ;

### Zahlen in Strings umwandeln

Forth bietet Ihnen die Zahlenausgabe in fast beliebigen Formaten. Schauen Sie sich zuerst die Tabelle der dafür nötigen bzw. verfügbaren Worte an:

WORD | Stack | Beschreibung
<# | -- | Leitet Wandlung Zahl->String ein
# | +n -- n' | generiert ein Zeichen aus n
#s | +n -- 0 | wandelt bis n = 0
#> | +n -- adr n | wandelt n in String mit Länge n
sign | flag -- | fügt gegebenenfalls Minus Zeichen ein
hold | char -- | fügt char in String ein

Beispiel: Zahl Hexadezimal ausgeben (8 Stellen):

    : .X ( n -- )
       base @ swap hex
       <# # # # # # # # # 120 hold 48 hold #> drop type
       base ! ;
    /* 120 = x ; 48 = 0 */

### Übung 2

1. Schreiben Sie ein Wort, daß eine Zahl mit 3 Kommastellen ausgibt.

2. Schreiben Sie ein Wort, daß nur positive Zahlen ausgibt.

### Lösungen 2

1. : .3places ( n - ) dup >r abs <# # # # 46 hold #s r> sign #> drop type ;

2. : .p ( n - ) dup 0>= if . else drop ." Error!" endif ;

### Eingabe von Zahlen

Um eine Eingabe vorzunehmen, die natürlich nicht unbedingt ein Zahl sein muß, bedienen wir uns des Wortes expect. Expect erwartet eine Adresse und die Anzahl der zu holenden Zeichen auf dem Stack. Die Eingabe wird abgebrochen, wenn entweder die Anzahl der Zeichen erreicht worden ist oder <cr> gedrückt wurde.

Vorher bedarf es noch der kurzen Erläuterung von allot: Allot reserviert (durch Hochsetzen des Dictionary-Pointers) eine gegebene Anzahl Bytes.

    create string 100 allot /* darf jetzt max. 100 Zeichen lang sein */

Die erste Eingabe:

    string 20 expect <cr>

Sie können jetzt maximal 20 Zeichen eingeben, bevor expect die Eingabe abbricht.

Ansehen können Sie sich das Ganze mit:

    string 1+ type <cr>

Strings in Zahlen wandeln:

WORD | Stack | Beschreibung
allot | n -- | setzt Dictionarypointer um n Bytes vor
pad | -- adr | Liefert die Adresse des Stringzwischenspeichers
expect | adr n -- | Holt n Zeichen von der Tastatur zu adr
type | adr -- | Gibt eine C-String (0term) auf der Console aus

### Strings in Zahlen wandeln

Manchmal kommt es vor, daß man eine Zahl aus einem String auslesen muß, z.B. aus GEM-Dialogen. Enthält der String einen Dezimalpunkt, wird die Position in der Variablen dpl gespeichert, andernfalls ist dpl = -1.

Zunächst zwei Beispiele:

    0 s" 12.3" convert . . <cr>
    s" 12.3" 1- number? . . <cr>
    dpl @ . <cr>

Ein bißchen seltsam erscheint das schon, oder?

Probieren Sie:

    s" 12.3" number? . . <cr>

Je nach Problem muß zwischen number? und convert gewählt werden.

WORD | Stack | Beschreibung
convert | 0 adr -- n adr' | Wandelt String ab adr in eine Zahl
number? | ^str -- n flag | Wandelt counted String in eine Zahl
dpl | -- adr | Variable, enthält Dezimalpunktposition

### Schleife do .. loop

Kommen wir jetzt zu der einfachsten Schleife in Forth, der do .. loop Konstruktion. Sie entspricht etwa dem BASIC-Äquivalent for i=0 to 100 (step x). Zunächst wieder ein Beispiel, das die Anwendung von do .. loop veranschaulicht.

Aufgabe: Ein Wort soll ab einer bestimmten Zahl die nächsten 10 Quadratzahlen ausgeben.

    : 10qz ( n -- )
       10 bounds  /* ToS: n+10 n */
       do    i dup * . space   loop ;

Neue Worte, aber es ist wieder mal halb so schlimm:

bounds
  - richtet den Stack für do .. loop ein. (: bounds ( n n' -- n+n' n ) over + swap ;)

do
  - erwartet Schleifenindex auf dem Stack

loop
  incrementiert Schleifenindex um 1

i
  - holt den Index der Schleife auf den Stack

space
  - gibt ein Leerzeichen aus (: space ( -- ) 32 emit ;)

Und jetzt die Quadratzahlen von 0 - 99:

    : 100qz ( -- )
       91 0 do  cr /* neue Zeile, die 90 wird auch gebraucht */
                i 10qz
          10 +loop ;

Stellt sich noch das Problem, wie ein do .. loop Schleife vorzeitig verlassen wird, nämlich mit leave. Leave veranlaßt das Programm, hinter dem nächsten loop fortzufahren. Damit das Wort auch seinen Zweck erfüllt, muß es "verpackt" werden.

Beispiel:

    : raus_bei_10  ( -- )
        100 0 do i 10 = if leave endif loop ;

Die Bedingung für den Ausstieg kann natürlich beliebig sein.

Wichtig!: Eine do .. loop Schleife NIE mit exit verlasssen!

WORD | Stack | Bedeutung
do | n2 n1 -- | Schleife von n1 bis n2
loop | -- | erhöht Schleifenindex um 1
+loop | inc -- | erhöht Schleifenindex um inc
i | -- index | Kopie des Schleifenindex auf den Stack
j | -- index' | Kopie des Index der äußeren Schleife
leave | -- | Verlassen einer Schleife

### FORTH und Datenstrukturen

Wenn Sie sich das Forth-Wörterbuch näher angeschaut haben, wunderten Sie sich vielleicht über das Fehlen von Strukturen, wie sie Sie aus anderen Sprachen kennen. Forth verfügt jedoch über Möglichkeiten, beliebige Strukturen zu entwerfen. Als Beispiel wähle ich ein eindimensionales Array. Tasten wir uns langsam heran (array ist in mForth bereits definiert).

Aufgabe: Es soll ein Array erzeugt werden, das für 20 Zahlen (LONG = 4bytes) Platz hat.

    create myArray 20 4 * allot

- create erzeugt einen Eintrag im Wörterbuch mit dem Namen myArray. Bei Aufruf, wird die Startadresse von myArray auf den Stack gelegt.

- allot reserviert 20 * 4 Bytes Speicher

So, Platz haben wir, jetzt wird das Array mit Leben, sprich Zahlen, gefüllt.

    9 myArray !     /* 1. Element erfordert keine Berechnung */
    8 myArray 4+ !  /* 2. Element */
    7 myArray 8+ !  /* 3. Element */

Das ganze lesend:

    myArray @ .     /* 1. Element */
    myArray 4+ @ .  /* 2. ...... */

Der Zugriff läßt sich vereinfachen. Dazu wird die Berechnung in einem Wort zusammengefaßt:

    : offset ( addr n -- addr+n*4)  4* + ;

    333 myArray 3 offset ! /* 3. Element mit 333 belebt */

So richtig elegant ist das aber immer noch nicht! Hier hilft uns das Wort does> zur Lösung des Problems:

    : array ( n -- <name> )
       create 4 * allot   /* reserviere Speicher */
       does>  ( n <array> -- addr' )
              swap 4* + :

Für unser Beispiel heißt dies:

    20 array myArray

    9 0 myArray !
    8 1 myArray ! /* usw. */

    0 myArray @ . /* lesen */

Abschließend ein 2-dimensionales Array:

    : 2array ( x y -- <name> )
       create  dup , /* Laenge y wird gebraucht */ * 4* allot
       does>   ( x y <2array> -- addr )
               dup >r @ * 4* swap 4* + r> 4+ + ;

    10 10 2array xyWerte

    123 0 0 xyWerte !
    126 1 0 xyWerte ! /* usw. */

WORD | Stack | Bedeutung
create | -- <name> | erzeugt einen Eintrag ins Wörterbuch
allot | n -- | setzt Dictionarypointer um n Bytes vor

### Strings

Aufgrund der Tatsache, daß das Betriebssystem des ST in C geschrieben wurde, mußten in mForth einige Kompromisse eingegangen werden. Im Standardforthstring enthält das erste Byte die Anzahl der Zeichen im String (Countbyte). Das hat den Nachteil, das nur Strings mit maximal 255 Zeichen kompiliert werden können. Für "normale" Strings verwendet mForth die gleiche Methode, die für eine Vielzahl von Anwendungen auch ausreicht. In mForth wird ein String ähnlich wie in C behandelt, das heißt, er schließt mit einem NULL-Byte.

Worte zum Stringhandling:

WORD | Stack | Beschreibung
s" | -- adr | Holt String aus dem Inputstream. Ende "
strlen | adr -- adr n | Liefert die Länge das Strings an adr
strcpy | adr1 adr2 -- | Kopiert String von adr1 nach adr2
strcat | adr1 adr2 -- | Fügt String adr1 an adr2 an
strncpy | adr1 adr2 n -- | Kopiert n Bytes von adr1 nach adr2
strcmp | adr1 adr2 -- flag | True, wenn beide Strings gleich sind
type | adr -- | gibt C-String auf der Console aus
count | adr1 -- adr1+1 n | Liefert Countbyte eines Forth(!)strings

Die obligatorischen Beispiele:

    create str1 32 allot
    create str2 32 allot
    create str3 128 allot

    s" ATOS Around "         str1 strcpy
    s" the Operating System" str2 strcpy

    str1 str3 strcpy  /* kopiere string 1 nach 3 */
    str2 str3 strcat  /* füge string 2 an */
    str3 type

    str1 strlen str2 swap strncpy

    str1 str2 strcmp .

### Anwendung

Ausgewählte Worte:

WORD | Stack | Beschreibung
integer | -- <name> | Erzeugt eine 'veränderliche' Konstante
to | n -- <name> | Weist name den Wert n zu
calloc | n -- adr true | false | Fordert Speicher an.
f_open | name mode -- hdl | Öffnet ein File. (handle 0> = OK)
f_read | hdl count adr -- read | Liest count Bytes aus File nach adr
f_close | hdl -- return | Schließt File.

Sie können den Sourcecode über die Kopierfunktion des ST-Guide in das Clipboard kopieren. Dieser Text (scrap.txt) kann in einen ASCII-Editor geladen und als bdays.4th o.ä. gesichert werden.

    /*
     * BDAYS.PRG
     * Eine kleine Anwendung in mForth
     *
     * Das Programm durchsucht ein File, in dem Geburtstage aufgelistet
     * sind und gibt den Namen aus, wenn Datum im Rechner und in der
     * Datei übereinstimmen.
     * Das Programm kann in den Autoordner kopiert oder als TOS-Programm
     * gestartet werden.
     */

    warning on                    /* doppelte Definitionen aufzeigen */
    mforth                        /* Suchreihenfolge */
    system macro on               /* dup, nip etc. als macros */
    true constant MAKE immediate  /* Programm soll erstellt werden */
    include system\date.4th       /* Funktionen rund ums Datum */

    mforth gemdos also            /* s.o.  */

    create fname ," c:\.\auto\days.dat" /* Name des Datenfiles */

    /* ---------------------------------------------------------
    Format:

    17.07 Karl Napp
    29.10 Michaela Mai

    Das File muß mit einer Leerzeile abschließen!
    Alle Daten im 5-stelligen Format angeben.
    ------------------------------------------------------------ */

    integer fhandle   /* Filehandle */
    integer filesize  /* Größe des DAT-Files */
    integer *data     /* Startadresse der Daten  */
    integer *str      /* Startadresse der Einträge */
    integer year      /* Aktuelles Jahr */
    integer month     /* Aktueller Monat */
    integer day       /* Aktueller Tag */
    variable found    /* Bool, True wenn irgendetwas gefunden wurde */

    /* Den akt. Monat im Klartext ausgeben */
    string-array monate
       ," Januar"
       ," Februar"
       ," März"
       ," April"
       ," Mai"
       ," Juni"
       ," Juli"
       ," August"
       ," September"
       ," Oktober"
       ," November"
       ," Dezember"
    end-string-array

    create tage
    31 c, 28 c, 31 c, 30 c, 31 c, 30 c,
    31 c, 31 c, 30 c, 31 c, 30 c, 31 c,

    /* String konvertieren, der String s" 12.09" double> läßt auf dem
       Stack 12 9 zurück */
    : double> ( addr -- tag monat )  1- number? drop 100 /mod ;

    /* Überspringe das nächste Linefeed in einem String */
    code skiplf ( addr -- addr' )
       .l a6 )+ a0 move
       begin .b 10 # a0 )+ cmpi 0<> while repeat
       .l a0 a6 -) move next end-code

    : nextday   ( monat tag -- monat' tag' )
       2swap over tage + 1- c@       /* max. Anzahl der Tage im Monat */
       2pick 2 = if  year 4 mod 0= if 1+ endif endif    /* Schaltjahr */
       >r 1+ dup r> >
       if    drop 1+ dup 12 > if drop 1 endif
             1     /* 1. des Monats  */
       endif 2swap ;

    : Today? ( month day m d -- month' day' m d flag )
       2pick over = >r 3pick 2pick = r> and ;

    /* Die eigentliche Suchroutine */
    : ?birthday ( -- )
       today to year to month to day
       ." Heute ist der " day . 8 emit ." . "
       month 1- monate 1+ type space year . cr cr

       found off      /* Noch nichts gefunden */
       *data to *str  /* CR durch 0bytes ersetzen */
       *data filesize bounds do i c@ 13 = if 0 i c! endif loop

       begin    month day               /* Heute */
                *str double> Today?     /* ??    */
                if    ."  Heute: " *str 6+ type cr
                      found on
                endif

                nextday Today?
                if    ." Morgen: " *str 6+ type cr
                      found on
                endif

                6 0 do nextday loop
                Today?
                if    ." In einer Woche: " *str 6+ type cr
                      found on
                endif

                4drop

                *str skiplf to *str
                *str 4+ *data filesize + >=
       until    found ?if key drop endif ;

    : main ( -- )
       fname 1+ 2 f_open dup to fhandle 0>
       if    32000 calloc   /* So groß wird days.dat wohl nicht werden */
             if    to *data
                   fhandle 32000 *data f_read to filesize
                   ?birthday
                   *data m_free drop
             endif
             fhandle f_close drop
       else  cr ." Days.dat nicht gefunden" key drop
       endif
       MAKE #if 0 return #endif
    ;

    mforth
    MAKE #if  system make \.\forth\bdays.prg bye  #endif

### Der nächste Teil des FORTH-Kurses

Der nächste Teil des FORTH-Kurses wird sich mit folgenden Themen beschäftigen:

- Tiefere Einblicke in Forth

- Noch mehr Schleifen

- Datenstrukturen

- Zugriff auf das Filesystem (GEMDOS-Funktionen)

- GEM/AES

- Eine sinnvolle Anwendung mit GEM-Funktionen

Für Anregungen und Fragen bin ich Ihnen dankbar.

Anrufen (06431-71188 ab 20:00 Uhr) oder schreiben.

EMail: Maus-Adresse von Rainer Saric

RS