C 64
Hilfsprogramm

Strubs — ein Precompiler für Basic-Programme

Bei Strubs, das steht für »strukturiertes Basic«, handelt es sich um einen sogenannten Precompiler, ein Programm, welches Programmtexte mit gewissen zusätzlichen Befehlen in normale, auf jedem Commodore 64 oder VC 20 ablauffähige Programme übersetzt.

Das Programm Strubs wurde ursprünglich zu einer Zeit entwickelt, als Begriffe wie Forth oder Pascal noch Fremdworte für den C 64 waren. Der Zweck war die Entwicklung von Programmen übersichtlicher, effizienter und bequemer zu gestalten.

Strubs bietet neue Basicbefehle

Auf der einen Seite ermöglicht es Strubs, auf sanftem Weg, das heißt im Rahmen des gewohnten Basic (aber ohne auf unübersichtliche Klimmzüge innerhalb des Commodore Basic angewiesen zu sein), also ohne gleich eine neue Programmiersprache lernen zu müssen, mit der Technik strukturierter Programmierung vertraut zu werden. Auf der anderen Seite ermöglicht es Strubs, sich mit der Arbeit mit Compilern vertraut zu werden.

Schließlich bietet die Form des Precompilers noch erhebliche Geschwindigkeitsvorteile gegenüber vergleichbaren Interpretererweiterungen. Um diese letzten beiden Punkte zu verstehen, ist es angebracht, auf die unterschiedlichen Arbeitsweisen von Interpretern und Compilern einzugehen.

Bekanntlich versteht der eigentliche Computer, das heißt hier der Mikroprozessor, nur die sogenannte Maschinensprache. Da diese aber extrem problemfern und unübersichtlich ist, hat man verschiedene höhere Programmiersprachen erfunden, um dem Programmierer seine Arbeit zu erleichtern. Damit aber ein in einer solchen Sprache geschriebenes Programm vom Computer verarbeitet werden kann, muß zunächst eine Übertragung in die Maschinensprache des Computers stattfinden. Dabei wird diese Übertragung wiederum von Programmen vorgenommen und zwar von Compilern oder von Interpretern. Diese beiden Programmarten unterscheiden sich grundlegend in ihrer Arbeitsweise.

Ein Interpreter besteht im wesentlichen aus einer Reihe von in Maschinensprache geschriebenen Unterprogrammen, einer Tabelle welche die erlaubten Befehle und die Adresse des zu jedem Befehl gehörenden Unterprogramms enthält, schließlich der Variablenverwaltung sowie der sogenannten Interpreterschleife.

Diese Schleife geht den Programmtext Schritt für Schritt durch. Zu jedem Befehl sucht sie in der Tabelle die zugehörige Unterprogrammadresse, ruft dieses Unterprogramm auf, holt den nächsten Befehl und so weiter, bis das Programmende erreicht ist. Man sieht also, daß ein großer Teil der Arbeit eines Interpreters im Suchen besteht: Suchen in der Befehlstabelle, Suchen in der Variablentabelle und nicht zuletzt Suchen nach Sprungzielen im, zu interpretierenden, Programm.

Diese ewige Sucherei führt nun dazu, daß Programme nur relativ langsam abgearbeitet werden. Eine Interpretererweiterung (wie etwa Siemens Basic) stellt nun einfach zusätzliche Befehlsroutinen zur Verfügung und erweitert die Befehlstabelle um die neuen Befehle und Adressen. Durch diese Erweiterung der Befehlstabellen wird jetzt aber leider auch der Zeitaufwand für das Suchen größer, so daß die Programme noch langsamer als bisher schon ablaufen. Simons Basic demonstriert dies sehr anschaulich. Ein Beispiel für eine Interpretererweiterung werden wir weiter unten besprechen.

Nehmen wir zur Illustration der Arbeitsweise eines Interpreters eine Programmzeile wie die folgende:

FOR I = 0 to 999: PRINT I: NEXT

Der Interpreter muß hier 1000mal die Befehlstabellen nach dem Befehl PRINT und 1000mal die Variablentabelle nach der Variablen I durchsuchen.

Compiler kontra Interpreter

Völlig anders arbeitet der Compiler: Er übersetzt ein Programm, das in einer Sprache geschrieben ist, welche nur der Programmierer versteht – dieses Programm nennt man Quellprogramm – in ein äquivalentes Programm – das Objektprogramm –, das (meist nur noch) die Maschine versteht. Diese beiden Begriffe – Quellprogramm und Objektprogramm – sollten wir uns gut merken; sie werden noch öfter auftauchen.

Der größte Teil der Sucharbeit kann nun ein für allemal bei der Übersetzung vom Compiler geleistet werden. Die benötigten Adressen der Befehlsroutinen, der Variablen und der Sprungziele sind für immer fest in das Objektprogramm eingebaut. Dadurch können compilierte Programme oft bis zu zehn- oder mehrmal schneller sein als entsprechende Interpreterprogramme.

Diesem beträchtlichen Gewinn an Geschwindigkeit steht allerdings ein nicht minder bedeutender Verlust an Bequemlichkeit gegenüber. Zum einen erfordert selbst die geringste Programmänderung eine vollständige Neuübersetzung des Programms. Dies allein kann bei umfangreichen Programmen erhebliche Zeit beanspruchen, zumal häufig auch noch diverse Zwischenschritte erforderlich sind, auf die wir hier nicht näher eingehen wollen. Zum anderen stellt das von einem einfachen Compiler erzeugte Objektprogramm für den Programmierer meist einen großen schwarzen Kasten dar, in den hineinzusehen ihm verwehrt bleibt. Er kann das Programm in der Regel nicht einfach unterbrechen, um sich bestimmte Variablenwerte anzusehen oder Variablen bestimmte Testwerte zuweisen, um damit dann einen kritischen Programmteil ausführen zu lassen, mal eben eine Zeile ändern und was der Annehmlichkeiten beim Programmtest mit einem Interpreter mehr sind. Bessere Compiler bieten zwar eine Reihe von Optionen und Hilfsprogrammen für die Fehlersuche und das Programmtesten an, jedoch bleibt auch hier, verglichen mit einem Interpreter, diese Arbeit reichlich unbequem. Ideal ist es sicherlich, äquivalente Interpreter und Compiler zur Verfügung zu haben. Auch gewisse Mischformen wie zum Beispiel bei der Sprache Forth sind hier interessant.

Strubs — eine Mischung von Interpreter und Compiler

Um nun aber auf das Programm Strubs zurückzukommen: Auch hier haben wir es in gewisser Hinsicht mit einer Mischform zu tun. Das selbst nicht lauffähige Quellprogramm, welches der Programmierer unter Benutzung der neuen Befehle erstellt, wird von Strubs nicht in Maschinensprache übersetzt, sondern in ein normales Basic-Programm, das dann wie bisher interpretiert wird. Dabei werden Programmteile, die keine Erweiterungen enthalten, mehr oder weniger unverändert übernommen. Dieses von Strubs erzeugte Objektprogramm kann nun wie jedes andere Basic-Programm — auch mit Hilfe von Toolkits — gelistet, ausgetestet und sogar geändert werden. Schließlich ist es dann noch möglich, dieses Objektprogramm mit Hilfe eines Basic-Compilers, wie zum Beispiel dem Austro Compiler, weiter zu übersetzen. Besonders hilfreich ist es, daß einander entsprechende Programmzeilen im Quellprogramm und im Objektprogramm gleiche Zeilennummern besitzen, so daß der Programmierer sich ohne Schwierigkeiten im Objektraum zurechtfinden kann. Gegenüber der Methode, den Basic-Interpreter zu erweitern, bietet dieses Verfahren Geschwindigkeitsvorteile: Diese ergeben sich einerseits aus der Tatsache, daß alle Kommentare und Leerzeichen gelöscht werden können, andererseits wird wie beim Compiler ein Teil der Sucharbeit während der Übersetzung erledigt. Dadurch werden zum Teil erst neue Anweisungen ermöglicht, deren Realisierung im Rahmen einer Interpretererweiterung zu aufwendig wäre.

Wer sucht, der findet: aber wann?

Daß sich die Suchzeit überhaupt in erträglichen Grenzen hält, liegt nun daran, daß der Programmtext selbst nicht durchsucht werden muß. Vielmehr braucht der Interpreter nur entlang der Kette aus Zeilennummern und Zeigern zur nächsten Zeile zu suchen, bis die gewünschte Zeilennummer gefunden ist (Bild 1). Sollte nun der Interpreter aber bei nicht erfüllter Bedingung in einer IF-Anweisung das zugehörige ELSE suchen, bei nicht erfüllter Eingangsbedingung einer FOR-Schleife das zugehörige NEXT oder zu einem WHILE das END-WHILE, dann müßte der gesamte Programmtext selbst durchsucht werden.

Bild 1. Struktur eines Basic-Programms und dessen Lage im Arbeitsspeicher

Interpreter durchlaufen jede Schleife mindestens einmal

Deshalb arbeiten die Basic-Interpreter im allgemeinen so, daß solche Blöcke — wie die FOR-Schleife — mindestens einmal durchlaufen werden. Deshalb muß bei solchen Interpretern — sofern sie überhaupt ein ELSE kennen — dieses in der gleichen Programmzeile wie das zugehörige IF stehen. Deshalb kennt zum Beispiel Simons Basic die REPEAT-UNTIL-Anweisung, die immer mindestens einmal durchlaufen wird, nicht aber die WHILE-Anweisung. Ein Precompiler aber kann bei der Übersetzung den Abschlußbefehlen eines Blockes — wie ELSE oder END-WHILE — ihre Zeilennummern zuordnen, so daß beim Programmlauf nicht mehr der Programmtext selbst, sondern nur die Kette der Zeilennummern durchsucht werden muß.

Vorübersetzung nicht nur beim Precompiler

Die Methode der Vorübersetzung zur Erhöhung der Laufgeschwindigkeit benutzt im übrigen auch der Basic-Interpreter des Commodore 64. Und zwar findet sich die Übersetzungsfunktion im Editor: Sofort bei der Eingabe einer Zeile werden die Basic-Befehle, die aus mehreren Zeichen bestehen, in nur 1 Byte lange Zeichen, die sogenannten TOKENS, übersetzt. Eine Liste dieser Tokens findet sich zum Beispiel im Programmierhandbuch zum VC 20. Diese Vorübersetzung bringt zwar einen schönen Gewinn an Geschwindigkeit, hat allerdings den Nachteil, daß Programmtexte nicht mehr mit komfortableren Editor- beziehungsweise Textprogrammen erstellt werden können. Für uns ist jedoch vor allen Dingen wichtig, daß diese TOKENS berücksichtigt werden müssen, falls der Befehlsvorrat von Strubs erweitert werden soll, oder falls Programme für Interpretererweiterungen wie Simons Basic bearbeitet werden sollen. Aber auf diesen Punkt werden wir ein anderes Mal ausführlicher eingehen.

Wenn wir mit Strubs arbeiten, haben wir es — wie bei jedem Compiler — mit (mindestens) drei Programmen zu tun: Dem Übersetzungsprogramm, dem Quellcode (Quellprogramm) und dem lauffähigen Objektprogramm. Diese Programme müssen sich nun irgendwie den zur Verfügung stehenden Speicherplatz teilen. Daß das Übersetzungsprogramm, um arbeiten zu können, im Hauptspeicher stehen muß, versteht sich von selbst.

Eine Möglichkeit wäre nun, daß das Übersetzungsprogramm das Quellprogramm von der Diskette einliest, und gleichzeitig das erzeugte Objektprogramm auf Diskette schreibt. Der Compiler zu Simons Basic arbeitet zum Beispiel nach dieser Methode. Da ein Compiler jedoch einen Programmtext in der Regel mindestens zweimal durchliest — man spricht in diesem Fall von 2-Pass-Compilern —, ist es günstiger, wenn das Quellprogramm sich ebenfalls im Hauptspeicher befindet. Diesen Weg gehen zum Beispiel Pascal 64 und Strubs. Um nun den zur Verfügung stehenden Platz aufzuteilen, benutzt zum Beispiel Pascal 64 eine sehr einfache und wirksame Methode: Der Compiler ist selbst in Basic geschrieben und enthält eine unsichtbare Zeile mit der Zeilennummer 0, die ihrerseits einen Sprung zum Übersetzungsprogramm enthält. Ein Pascalprogramm wird nun einfach mit den Zeilennummern zwischen 1 und 9999 in das Compilerprogramm eingefügt.

Dieses Verfahren hat allerdings den Nachteil, daß Programme immer nur zusammen mit dem Compiler abgespeichert und editiert werden können. Insbesondere ist es damit nicht möglich, Quellprogramme aus fertigen Bausteinen (Modulen) zusammenzusetzen.

Strubs geht andere Wege

Aus diesem Grund wurde für Strubs ein anderer Weg gewählt: Entsprechend Bild 1 wurde der Speicher des Commodore 64 in drei Bereiche aufgeteilt. Am Anfang des Arbeitsspeichers steht das Programm Strubs (Pointer in Zelle 43/44). Der Edit-Bereich für Quellprogramme beginnt bei (Wert der Variablen EA). Daran anschließend befindet sich der (gemeinsame) Variablenbereich (Pointer in Zelle 45/46). Um nun vom Edit-Bereich aus bequem in den anderen Speicherbereich umschalten und die Übersetzung starten zu können, benutzt Strubs selbst eine kleine Interpretererweiterung, die, wie versprochen, kurz vorgestellt werden soll.

Die Eingabe von »!« bewirkt nun dasselbe wie die Befehlsfolge »POKE 44,8: RUN«. Das entsprechende Assemblerlisting findet sich in Bild 2. Das kleine Programm »Erweiterung« holt zunächst den nächsten Befehl. Dann muß für die Routine »Befehl ausführen« der Status gerettet werden, da die CHARGET-Routine damit wichtige Informationen übermittelt. (Dies ist wichtig und wurde in dem unten erwähnten Buch übersehen.) Nachdem verglichen wurde, ob ein neuer Befehl vorliegt, wird dann entsprechend zum normalen Programmverlauf oder zur Erweiterungsroutine verzweigt. Für eigene Versuche mit Interpretererweiterungen können an dieser Stelle beliebige Maschinenprogramme (gegebenenfalls mit weiteren Decodierungen) gesetzt werden. Nur sollte zum Abschluß – anders als hier, wo ein Basic-Befehl aufgerufen wird – ein Sprung zur Interpreterroutine $A7E4 erfolgen, wo dann der nächste Befehl bearbeitet wird. Um nun die Erweiterung in den Basic-Interpreter einzubinden, benötigen wir dann nur noch eine kurze Initialisierungsroutine, die den Zeiger in $0308 auf den Anfang der Erweiterung setzt.

Erweiterung:
02C0  207300  JSR 0073  ; Charget, nächstes Zeichen holen
02C3  08      PHP       ; Status retten
02C4  C921    CMP #21   ; »!«, neuer Befehl?
02C6  F004    BEQ 02CC
02C8  28      PLP       ; nein, dann Status wiederherstellen
02C9  4CE7A7  JMP A7E7  ; und normalen Befehl ausführen
02CC  28      PLP
02CD  A908    LDA #08   ; Erweiterungsroutine:
02CF  852C    STA 2C    ; entspricht Poke 44,8: RUN
02D1  A98A    LDA #8A   ; RUN-TOKEN
02D3  4CE7A7  JMP A7E7  ; Befehl ausführen

INIT:
02EE  A9C0    LDA #C0   ; Erweiterung, Low Byte
02F0  8D0803  STA 0308
02F3  A902    LDA #02   ; Erweiterung, High Byte
02F5  8D0903  STA 0309
02F8  60      RTS
Bild 2. Interpreter-Erweiterung

Wer selbst solche Erweiterungen entwickeln möchte, findet weitere Informationen und viele Anregungen in dem Buch »64 Intern« von Data Becker. Für weitergehend Interessierte empfiehlt sich die gut verständliche Einführung »Compilerbau« von N. Wirth, Teubner, Stuttgart 1981.

Strubs stellt sich vor

Abschließend wollen wir nun das Programm Strubs kurz vorstellen. Am Anfang der Programmentwicklung standen folgende Vorstellungen, die durch das Programm erfüllt werden sollten:

  1. Unabhängigkeit von Zeilennummern
  2. Unterstützung strukturierter Programmierung
  3. Unterstützung modularer Programmentwicklung
  4. Erweiterung der Dokumentationsfähigkeit des Programmtextes. Dabei sollte das Programm
  5. einfache Handhabung gewährleisten und
  6. effiziente Fehlersuche ermöglichen.

Die Unabhängigkeit von Zeilennummern wird erreicht durch die Verwendung beliebig langer Labels oder relativer Sprünge anstelle von Zeilennummern.

Die wichtigsten Kontrollstrukturen höherer Programmiersprachen werden von Strubs zur Verfügung gestellt:
IF – THEN – FI
IF – THEN – ELSE – FI
WHILE – EWHILE
REPEAT – UNTIL
LOOP – EXIT (beliebig oft) – ELOOP
CASEOF – OF (beliebig oft) – ELSE (optional) – ECASE

Durch die Unabhängigkeit von Zeilennummern und eine EXTERN-DECLARATION wird das Anlegen einer Modulbibliothek – sowohl auf Quellprogramm- als auch auf Objektprogrammebene – unterstützt.

Mit Strubs werden Sie ein vielseitiges Werkzeug in Händen halten

Der Dokumentationsfähigkeit des Programmtextes dienen neben den bereits erwähnten Marken und Kontrollstrukturen ein Tabulator und Kommentare an beliebiger Stelle auch innerhalb einer Zeile, ja selbst innerhalb eines Variablennamens (zum Beispiel A'US'G'ABE'% = AG%).

Programmtexte können wie gewohnt mit dem normalen Basic-Editor geschrieben werden.

Schließlich werden wir zur Illustration der Erweiterung des Befehlssatzes von Strubs noch eine MAKRO-Funktion implementieren. Von besonderer Bedeutung ist, daß das Programm von Anfang an unter dem Aspekt möglichst einfacher Erweiterbarkeit konzipiert wurde. Damit konnte das Programm im Bootstrapping-Verfahren entwickelt werden, so daß es jetzt selbst sowohl als Quellprogramm wie auch als Objektprogramm vorliegt. Wem es Spaß macht, der mag Strubs einfach auch als ein generelles Übersetzungsprogramm zur Aufbereitung von Programmtexten auffassen und seine gegenwärtigen Funktionen als Beispiel möglicher Implementationen.

Die Befehlsstruktur

Gehören Sie auch zu denjenigen, die sich manchmal ein Programm aus einer Zeitschrift vornehmen, um zu analysieren, wie es arbeitet oder um eventuell Teile des Programms für eigene Programmprojekte zu verwenden? Dann erinnern Sie sich bestimmt an Programme, bei denen Sie sich verzweifelt von Sprung zu Sprung bewegen und nach nicht allzu langer Zeit vollkommen den Überblick verlieren. Oder vielleicht kennen Sie folgende Situation: Sie schreiben ein Programm und erinnern sich angesichts eines bestimmten Problems, daß Sie ein ganz ähnliches Problem schon einmal in einem anderen Programm gelöst haben. Aber sobald Sie sich den alten Programmtext vornehmen, um den entsprechenden Programmteil in ihr neues Programm zu übernehmen, müssen Sie enttäuscht feststellen, daß diese spezielle Problemlösung so sehr in das Programmgeflecht verwoben ist, daß es Ihnen weitaus einfacher scheint, den entsprechenden Programmteil vollkommen neu zu entwickeln.

Die Ursache für solche Erscheinungen liegt zum Teil darin, daß viele Basic-Programme mehr oder weniger aus der Sicht des Computer der »Basic-Maschine« — direkt am Computer nach dem Verfahren von Versuch und Irrtum entwickelt werden. Das kann in Einzelfällen sogar soweit führen, daß man zum Schluß zwar sieht, daß das Programm läuft, aber selbst nicht so recht weiß, warum eigentlich und wie es funktioniert. Der Hauptgrund für solche Unübersichtlichkeit aber liegt in der Verwendung zahlreicher wilder Sprünge und ausgefallener Programmier-Tricks. (Daß die Verwendung von GOTO-Anweisungen den mathematischen Beweis für die Korrektheit von Programmen praktisch unmöglich macht, ist für den Informatiker interessant, braucht uns hier aber nicht zu interessieren).

Den entgegengesetzten Weg geht die strukturierte Programmierung. Sie bedeutet vor allem sorgfältige Planung und den Verzicht auf GOTOs und unübersichtliche Programmiertricks. Hier steht die systematische Analyse des Problems im Vordergrund. Die eigentliche Codierung, das heißt die Formulierung des Programmtextes in einer bestimmten Programmiersprache, spielt nur eine untergeordnete Rolle.

In der Problemanalyse geht es darum, ein gegebenes Problem in relativ selbständige Teilprobleme zu zerlegen und deren Beziehungen zueinander festzulegen. Den Aufbau des Programms Strubs mit den jeweiligen Zeilennummern können Sie Bild 3 entnehmen. Das komplette Objektprogramm ist ebenfalls abgedruckt (siehe Listing 1).

Bild 3. Aufbau von Strubs

Entsprechend setzt sich das strukturierte Programm aus einer Reihe möglichst selbständiger Programmeinheiten zusammen. Dieses Vorgehen spiegelt sich im Konzept der Blöcke und Module.

Ein Block ist eine Anweisung oder eine Folge von Anweisungen mit genau einem Eingang und genau einem Ausgang. Das heißt man darf weder in einen solchen Block hineinspringen, noch aus diesem Block herausspringen. Solche Blöcke können entweder aneinander gereiht oder beliebig tief ineinander geschachtelt werden; sie dürfen sich aber nicht überschneiden. In letzterer Hinsicht verhält es sich mit diesen Blöcken also genauso, wie bei den bekannten FOR-Schleifen in Basic.

Ein strukturiertes Programm besteht nun ausschließlich aus einer geordneten Hierarchie solcher Blöcke. Der kleinste mögliche Block besteht aus einer einzelnen Anweisung, wie zum Beispiel PRINT "Text". Der größte, umfassendste Block besteht aus dem Programm selbst.

Da ist zunächst einmal die einfache IF-Anweisung, die schon von Basic her bekannt ist. Dieses normale Basic-IF kann natürlich wie alle Basic-Befehle weiterhin benutzt werden. Zusätzlich bietet Strubs aber eine erweiterte Form, bei welcher der THEN-Teil nicht auf den Rest einer Programmzeile begrenzt ist, sondern beliebig viele Zeilen umfassen kann, die durch den Befehl '!FI' — einfach ein umgedrehtes IF — abgeschlossen werden. Ein Beispiel:

10 ! IF X=Y THEN
20 :    PRINT "X und Y"
30 :    PRINT SIND GLEICH"
...
99 !FI

Ist die Bedingung hinter IF erfüllt, so werden die Zeilen zwischen der IF- und der FI-Anweisung ausgeführt, ansonsten wird das Programm sofort hinter der FI-Zeile fortgesetzt.

Daneben existiert selbstverständlich auch die vollständige Form

10 !IF X=Y THEN
20:     PRINT "GLEICH’
50 !ELSE
60 :    PRINT "UNGLEICH"
99 !FI

Ist die Bedingung erfüllt, dann wird der Block zwischen IF und ELSE ausgeführt, sonst der Block zwischen ELSE und FI.

Für den Fall, daß mehr als nur zwei Fälle zu unterscheiden sind, bietet Strubs die CASE-Anweisung:

10 !CASEOF X<0 THEN
15 :    PRINT "KLEINER ALS 0”
...
40 ! OF X=0 THEN
45 :    PRINT "GLEICH 0"
...
60 ! OF X>0 AND Y<XTHEN
65 : PRINT "X>0 UND Y<X"
...
80 ! ELSE
85 :    PRINT "KEINER DER FÄLLE TRIFFT ZU"
99 ! ECASE

Mit dieser Struktur können beliebig viele Fälle unterschieden werden, wobei jedes OF mit einer beliebigen Bedingung verbunden werden kann. Es sollte aber darauf geachtet werden, daß sich die Bedingungen gegenseitig ausschließen (sonst wird das erste Auftreten einer erfüllten Bedingung gewählt). Nach der Bearbeitung des entsprechenden Falles wird das Programm immer hinter ECASE fortgesetzt. Die Möglichkeit, daß keiner der Fälle zutrifft, kann mit Hilfe der ELSE-Anweisung behandelt werden. Ist dies nicht erforderlich, kann der ELSE-Teil auch entfallen.

Damit kommen wir nun zu den Schleifen. Die FOR-Schleife kann wie bisher benutzt werden. Die WHILE-Schleife wird durchlaufen, solange die Bedingung erfüllt ist. Anschließend wird das Programm hinter EWHILE fortgesetzt. Da die Bedingung am Anfang der Schleife abgefragt wird, kann es vorkommen, daß die Schleife auch überhaupt nicht durchlaufen wird. Ein Beispiel:

10 ! WHILE X<5 !DO
20 :  PRINT "IMMER NOCH KLEINER ALS 5"
30 : X = X + 1
...
99 !EWHILE

Von der WHILE-Schleife unterscheidet sich die REPEAT-Schleife in zwei Punkten: Erstens wird die Schleife durchlaufen, bis die Bedingung erfüllt ist, also solange sie nicht erfüllt ist. Zweitens wird die Bedingung erst am Ende der Schleife abgefragt, so daß die Schleife immer mindestens einmal durchlaufen wird. In diesem wie im nächsten Beispiel bezieht sich die Zeile 30 auf den Fall, daß X beim Eintritt in die Schleife größer als 5 ist:

10 ! REPEAT
20 :  PRINT "X KLEINER ALS 5"
30 : PRINT "VIELLEICHT ABER AUCH NICHT"
40 : X = X+1
...
99 ! UNTIL X >= 5

Eine weniger weit verbreitete, aber sehr mächtige Schleifenstruktur stellt die LOOP-Schleife dar (sie befindet sich zum Beispiel in der Programmiersprache ADA):

10 ! LOOP
30 :  PRINT "EVENTUELL GROESSER ALS 5"
40 :  IF X>=5 THEN !EXIT
50 :  PRINT"KLEINER ALS 5"
60 :  X = X+1
99 !ELOOP

Es handelt sich dabei um eine Endlosschleife, welche mit Hilfe des Befehls EXIT verlassen werden kann. Diese Schleife bietet im wesentlichen zwei Vorteile: Zum einen muß die Bedingung nicht entweder am Anfang oder am Ende der Schleife stehen, sondern kann an jeder beliebigen Stelle innerhalb des Blockes abgefragt werden. Darüber hinaus ist das Beenden der Schleife nicht nur von einer Bedingung abhängig, sondern die LOOP-Schleife kann beliebig viele EXIT-Anweisungen enthalten (dadurch wird nicht die oben erwähnte Forderung nach nur einem Ausgang verletzt, da das Programm in allen Fällen hinter dem ELOOP fortgesetzt wird). Damit eignet sich diese Konstruktion insbesondere gut für die Behandlung von Ausnahmen wie zum Beispiel von Eingabebefehlen etc. (eine Angelegenheit, die zum Beispiel in Pascal recht umständlich sein kann, falls man auf GOTOs verzichten will oder muß).

In Bild 4 (das Zeichen ' kennzeichnet Kommentare) sehen Sie ein Beispiel für geschachtelte LOOP-Schleifen. Die Ausführung einer EXIT-Anweisung bewirkt die Fortsetzung des Programms bei der ersten Zeile hinter derjenigen Schleife, welch© diese EXIT-Anweisung am nächsten umschließt. Im Beispiel enthält die äußere Schleife zwei EXIT-Anweisungen — eine davon vor, die andere hinter der inneren Schleife. Die innere Schleife enthält eine EXIT-Anweisung. Grafisch lassen sich blockstrukturierte Programme am besten durch Struktogramme — anstelle der verbreiteten Flußdiagramme — darstellen. Das Struktogramm für die LOOP Schleifen finden Sie in Bild 5. Über die Diagramme der anderen Strukturen und den Umgang mit Struktogrammen können Sie sich an anderer Stelle in dieser Zeitschrift oder in den unten aufgeführten Büchern informieren. Kommen wir nun zu den Modulen. Dabei handelt es sich um besondere Blöcke, die ein bestimmtes Teilproblem — beispielsweise das Zeichnen einer Linie in einem Grafikprogramm — unter möglichst weitgehender Unabhängigkeit vom restlichen Programmtext bearbeiten. Stellen Sie sich vor, Sie finden in einer Zeitschrift ein Pascal-Programm zur Einstellung von Grafiken. Dieses Programm benutzt zum Beispiel die Anweisung PLOT (X,Y) zum Zeichnen eines Punktes mit den Koordinaten X und Y. Ihr Freund möge eine Sprache Super-Pascal besitzen, die diese Anweisung standardmäßig enthält. Er tippt das Programm ein, es läuft — fertig. Sie selbst besitzen aber nur ein mageres Mini-Pascal, das diesen Befehl nicht kennt. Nun, mit Pascal ist das kein Problem: Sie schreiben sich eine Procedur PLOT (X,Y) fügen diese in das Programm ein — fertig. An dem Programmtext selbst brauchen Sie nicht die geringste Änderung vorzunehmen. Ja, brauchen ihn nicht einmal näher anzusehen. Woran liegt das?

510 '*********************************
520 '* GESCHACHTELTE LOOP-BLOECKE    *
530 '*********************************
540 '
620 !LOOP 'L1
630 :   PRINT"AEUSSERE LOOP1"
640 :   IF X=1 THEN !EXIT 'LOOP1
650 :   !LOOP 'L2
660 :     PRINT "INNERE LOOP2"
670 :     IF X=0 THEN !EXIT 'LOOP2
680 :   !ELOOP ' L2
690 '   HIER WIRD PROGR. NACH EXIT LOOP2 FORTGESETZT
700 :   X=X+1
710 :   !IF X=2 THEN
720 :       PRINT "LOOP1 VERLASSEN:":!EXIT 'LOOP1
730 :   !FI
740 :   X=X+1
750 !ELOOP 'L1
760 PRINT"HIER WIRD PROGRAMM NACH EXIT LOOP 1 FORTGESETZT"
770 '
Bild 4. Geschachtelte Loop-Schleife
Bild 5. Struktogramm der Loop-Schleife

Vom Problem her — dem Erstellen einer Grafik — ist das Zeichnen eines Punktes das Zeichnen eines Punktes. Das einzige, was interessiert, ist, daß dazu zwei Koordinaten erforderlich sind. Dieser Tatsache trägt die Sprache Pascal dadurch Rechnung, daß sie keinen Unterschied macht zwischen dem Aufruf von vorgegebenen Standardanweisungen und selbst definierten Prozeduren.

Wenn Sie in einem Basic-Programm irgendwo eine Zeile PRINT "TEXT” stehen haben, erwarten Sie selbstverständlich, daß dadurch nicht 50 Zeilen weiter der Wert der Variablen A verändert wird. Entsprechend sorgt nun Pascal dafür, daß eine selbst definierte Prozedur genausowenig Auswirkungen auf andere Programmteile hat wie der Aufruf einer Standard-Anweisung. Die interne Arbeitsweise einer solchen Prozedur wird vor der Programmumgebung genauso versteckt, wie dies bei der internen Arbeitsweise von im Sprachumfang enthaltenen Anweisungen der Fall ist. Entsprechend nennt man dieses Konzept auch »Information Hiding«. Programmiersprachen wie ADA, MODULA oder SIMULA bieten in dieser Hinsicht noch sehr viel weitergehende Möglichkeiten als Pascal.

Schnittstellen:

Der Datenaustausch mit der Umgebung eines Moduls erfolgt über genau definierte Schnittstellen. Bei einer solchen Schnittstelle handelt es sich um eine Menge derjenigen Annahmen, die die Programmumgebung über ein Modul macht — das heißt welche Daten es als Eingabe erwartet, welche Daten es daraufhin wieder ausgibt und welche anderen Module es seinerseits benötigt.

Modulbibliothek:

Die relative Eigenständigkeit solcher Module sorgt nun nicht nur für einfache Änderbarkeit und Erweiterbarkeit, sondern ermöglicht auch das Anlegen einer sogenannten Modulbibliothek. Eine solche Bibliothek enthält eine Reihe von Programmbausteinen, die je nach Bedarf in zu entwickelnde Programme eingefügt werden können. Dabei kann es sich um Sortierroutinen, Grafik-Routinen, mathematische und statistische Routinen und so weiter handeln. Aber auch die Entwicklung von Spielen läßt sich auf diese Weise vereinfachen: Man kann Bibliotheken fertiger Sprites, von eigenen Zeichensätzen oder von diversen Soundroutinen anlegen.

Das wichtigste Hilfsmittel zur Unterstützung modularer Programmentwicklung stellen sicherlich die lokalen Variablen dar. Leider gibt es solche nicht in Basic und auch Strubs kann keine lokalen Variablen bieten. So ist es auch weiterhin erforderlich, beim Einsetzen oder Ändern eines Moduls darauf zu achten, ob und an welchen Stellen Variablen des Moduls in anderen Programmteilen benutzt werden, und gegebenenfalls Umbenennungen vorzunehmen. Der zweite große Nachteil von Basic — die leidigen Zeilennummern — braucht uns dagegen nur noch wenig zu beschäftigen. Strubs bietet alle Möglichkeiten, die erforderlich sind, um ein Programm vollkommen unabhängig von Zeilennummern zu schreiben Als erstes sind da natürlich die oben besprochenen Kontrollstrukturen zu nennen. Darüber hinaus können bei allen Sprüngen Zeilennummern durch Labels (Marken) ersetzt werden. Solche Labels werden durch das Zeichen »£« gekennzeichnet und abgeschlossen durch ein Leerzeichen, Doppelpunkt, Komma oder Zeilenende. Die dürfen zwar reservierte Basic-Worte enthalten, dann können sich aber wegen der in der letzten Folge erwähnten Tokens bei der Ausgabe der Markentabelle seltsame Effekte ergeben. Die Labels werden definiert, indem sie an den Anfang einer Zeile gesetzt werden und können beliebig lang sein:

10   £X-AUSGEBEN:
20 : PRINT "X:";X
30   RETURN
...
200 X=1:GOSUB £X-AUSGEBEN
210 X=2:GOSUB £X-AUSGEBEN

Schließlich bietet Strubs noch die Möglichkeit relativer Sprünge. Diese dienen vor allem dazu, kurze Schleifen innerhalb einer einzigen Zeile zu konstruieren, ohne dafür extra ein Label zu definieren:

90 NC=NC+1:C=PEEK(NC):IF C>0 THEN Z$+CHR$(C): GOTO £THIS

Der Befehl GOTO £THIS bewirkt einen Sprung an den Anfang derjenigen Zeile, in der dieser Befehl steht.

Da bei der Arbeit mit Strubs Quellprogramme in der Regel weit umfangreicher als die Objektprogramme sind, bietet Strubs die EXTERN-DEKLARATION, die es ermöglicht, Module und Programmteile getrennt zu übersetzen und erst auf der Objektprogrammebene zusammenzufügen. Hierbei müssen die einzelnen Programmteile allerdings verschiedene Zeilennummern belegen. In der Extern-Deklaration wird ein Name vereinbart, unter dem ein Programm ein externes Modul ansprechen kann. Diesen Namen wird die Einsprungadresse (bei Maschinenprogrammen) beziehungsweise die Zeilennummer bei Basic-Routinen zugewiesen:

20 REM VEREINBARUNG:
30 ! EXT: £MAPRO:740,£PLOT: 50000
...
90 REM AUFRUF:
99 SYS £MAPRO: X=13:Y=90:GOSUB £PLOT

Kommen wir abschließend zur Dokumentation: Vom Hobby-Programmierer kann kein Mensch erwarten, daß er Berge von Dokumentationsmaterial anlegt, die den Umfang des Programmtextes um ein Vielfaches übersteigen. Deshalb ist es gerade hier wichtig, Programme weitgehend selbstdokumentierend zu schreiben. Im Gegensatz zu höheren Programmiersprachen mit ihren zahlreichen Deklarationspflichten ist der Basic-Programmierer nahezu ausschließlich auf Kommentare angewiesen. Da Strubs Kommentare bei der Übersetzung eleminiert, stehlen diese weder Speicherplatz noch Laufzeit. Der Programmierer kann also ohne Bedenken einen exzessiven Gebrauch von Kommentaren machen.

Kommentare werden gekennzeichnet durch das Zeichen »'«. Steht dieses Zeichen direkt am Zeilenanfang, so wird die ganze Zeile gelöscht. Sonst wird der Programmtext bis zum zweiten »'« oder bis zum Zeilenende überlesen. Außer innerhalb von Befehls- und Markennamen können Kommentare an jeder beliebigen Programmstelle eingefügt werden. Kommentare, die in das Objektprogramm übernommen werden sollen, können wie bisher mit REM in den Programmtext eingefügt werden. Beispiel:

10 'DIESE ZEILE WIRD VOLLSTÄNDIG GELÖSCHT
20 A'US'G'ABE'$="ENTSPRICHT AG$" 'KOMMENTAR

Die Lesbarkeit von strukturierten Programmen wird verbessert durch das Einrücken von Zeilen entsprechend der Blockstruktur. Hierzu dient der Tabulator (Bild 4): Ein Doppelpunkt am Zeilenanfang gefolgt von Leerzeichen.

Wenn Sie Quellprogramme schreiben, achten Sie vor allem auf folgendes: Jeder der neuen Befehle muß durch ein Ausrufezeichen gekennzeichnet werden, zum Beispiel !REPEAT, und benötigt — außer !DO — eine eigene Programmzeile. Folgende Konstruktion ist also zum Beispiel nicht erlaubt:

40 !REPEAT X = X +1 !UNTIL X > 5

Marken beginnen grundsätzlich mit einem Pfund-Zeichen £.

Um die Korrektheit von Konstruktionen zu überprüfen, können Sie die Syntax-Diagramme in Bild 6 benutzen. Wenn sich ein Weg entlang der Linien finden läßt, der der Konstruktion entspricht, dann ist diese in Ordnung. Sie können sich aber auch an den verschiedenen Beispielen im Rahmen dieser Artikelfolge orientieren.

Das beste Beispiel für ein Quellprogramm erhalten Sie, wenn Sie die Programmdiskette mit dem Programm Strubs bestellen (wird allerdings erst ab der nächsten Ausgabe angeboten). Diese Diskette enthält neben dem lauffähigen Objektprogramm, das in der letzten Ausgabe abgedruckt wurde, auch das ausführlich dokumentierte Quellprogramm von Strubs, dessen Abdruck aus Platzgründen nicht möglich ist.

Bild 6. Syntaxdiagramme der Befehle

Programmentwicklung mit Strubs

Bei der Blockschachtelung ist darauf zu achten, daß sich verschiedene Blöcke nicht überschneiden dürfen und daß jeder Block korrekt abgeschlossen wird. Hierbei kann man sich immer das Beispiel der FOR-NEXT-Schleifen in Basic vor Augen halten. Vollkommen unmöglich ist beispielsweise folgende Konstruktion:

10 !REPEAT
20 : !WHILE ... !DO
30 !UNTIL ...
40 : !EWHILE

Nun wird es aber allmählich Zeit, mit der Praxis zu beginnen. Laden Sie das Programm Strubs in Ihren Computer und starten es mit »RUN«. Nun erscheint ein Menü. Geben Sie hier »E« ein, um in den Editbereich zu gelangen (siehe dazu die erste Folge). Der Computer meldet sich mit »READY«, das heißt also, Sie befinden sich jetzt im Direktmodus. Hier können Sie nun (fast) so arbeiten, als sei Strubs gar nicht vorhanden. Geben Sie zunächst »NEW« ein. Jetzt können Sie das kleine Programm aus Bild 7 eintippen und wie sonst gewohnt mit »SAVE "RENUMBER.QP”,8« abspeichern.

Mit diesem »QP« hat es folgende Bewandnis: Bei Compilern ist es allgemein üblich, die verschiedenen Files, die zu den einzelnen Phasen der Übersetzung gehören, einheitlich zu kennzeichnen, der Austro-Compiler, arbeitet zum Beispiel mit den Files »name«, »p/name«, »z/name« und »c/name«. Um Quellprogramme und Objektprogramme auseinanderhalten zu können, sollten Sie sich entsprechend von Anfang an daran gewöhnen, dem Programme immer ein »QP« für Quellprogramm beziehungsweise ein »OP« für Objektprogramm hinzuzufügen. Nun kann das Programm übersetzt werden. Geben Sie ein »IRETURN« und es erscheint das Menü von Strubs. Die Übersetzung wird mit »U« gewählt. Strubs fragt nun nach dem Namen für das Objektprogramm. Geben Sie ein: RENUMBER.OP. Da das übersetzte Programm direkt auf Diskette geschrieben wird, achten Sie darauf, daß die Floppy eingeschaltet ist. Nun erscheint auf dem Schirm die Meldung »1. Lauf«, gefolgt von der Ausgabe der Blockstruktur. Nach Beendigung des 2. Laufs sollte die Meldung »0 FEHLER« erscheinen. Ist dies der Fall, dann können sie mit »E« wieder in den Edit-Bereich gelangen. Hier steht immer noch das Quellprogramm. Um sich das übersetzte Programm anzusehen, laden Sie es mit »LOAD "RENUMBER.OP",8«. Es sollte mit dem Listing in Bild 8 übereinstimmen. Aber starten sie das Programm nicht.

54990 '******** renumber ***************
55000 £renumber: print"{clr}         ***** renumber ******"
55010 za=40*256+1:input"{down}{down} startnr.";zn:input " schrittweite";sw
55020 !loop
55030 :  ifpeek(za+1)=0then !exit 'fertig
55050 : h%=zn/256:poke za+2,zn-h%*256:poke za+3,h%:za=peek(za)+256*peek(za+1)
55050 : zn=zn+sw
55060 !eloop
55070 return
Bild 7. Das Quellprogramm des RENUMBER-Befehls
55000 print"{clr}         ***** renumber ******"
55010 za=40*256+1:input"{down}{down} startnr.";zn:input " schrittweite";sw
55020 :
55030 ifpeek(za+1)=0then55061
55050 h%=zn/256:poke za+2,zn-h%*256:poke za+3,h%:za=peek(za)+256*peek(za+1)
55050 zn=zn+sw
55060 goto55020
55061 :
55070 return
Bild 8. Das von Strubs erzeugte Objektprogramm. Wie es in Strubs einzufügen ist, steht auf Seite 107.

Jetzt übersetzen Sie einmal genauso verschiedene kleine Testprogramme — zum Beispiel die Beispiele aus der letzten Folge — und sehen sich die Ergebnisse an. Dabei werden Sie feststellen, daß einige Bedingungen im Objektprogramm in negierter Form erscheinen. Daß Basic keine boolschen Variablen kennt, hat eine wichtige Konsequenz: Beim Test, ob eine Variable ungleich 0 ist, darf man nicht — wie dies normalerweise häufig in Basic formuliert wird — beispielsweise schreiben

IF A THEN ...,

sondern muß bei jeder Bedingung die vollständige Form

IF A<>0 THEN ...

verwenden. Dies liegt daran, daß die Bedingung NOT(A) außer für —1 immer erfüllt ist.

Zweitens kann man sehen, daß in den Objektprogrammen manchmal neue Zeilen auftauchen, die das Quellprogramm nicht enthielt. Strubs generiert solche Zeilen als Sprungziele. Damit immer Platz für solche Zeilen ist, sollte der Abstand der Zeilennummern im Quellprogramm immer mindestens 2 betragen.

Ist bisher alles wie oben beschrieben verlaufen, dann können Sie sich freuen. Sind irgendwelche Fehler aufgetreten, dann vergleichen Sie noch einmal genau das Testprogramm mit dem Listing in Bild 7 und hoffen Sie, daß der Fehler hier liegt. Wenn Sie keine Abweichungen feststellen, dann haben Sie Pech — Sie haben das Programm Strubs falsch eingetippt.

Wie steht es aber mit Fehlern im Quellprogramm? Syntax-Fehler können sich auf drei verschiedene Arten bemerkbar machen: Vor allem Fehler, die nicht mit den neuen Befehlen zusammenhängen, führen wie gewohnt beim Lauf des Objektprogramms zu den bekannten Fehlermeldungen. Fehler in bezug auf die neuen Befehle quittiert Strubs mit Abbruch der Übersetzung, falls eine Fortsetzung nicht sinnvoll erscheint, oder mit Eintragung in eine Fehlerliste und gleichzeitiger Kennzeichnung der fehlerhaften Zeile im Objektprogramm. Die Fehlerliste kann man sich mit »F« ansehen.

Ein Abbruch der Übersetzung mit entsprechender Fehlermeldung am Bildschirm erfolgt vor allem bei Verstößen gegen die Blockstruktur (und bei Speicherplatzproblemen wie Stack-Overflow oder Listen voll). Bei Fehlern mit den Blöcken — zum Beispiel vor einem ELSE fehlt das IF oder zu einem WHILE fehlt das EWHILE etc. — gibt es ein Problem, das nicht nur bei Strubs, sondern generell bei allen Compilern auftaucht. Der Fehler wird nicht an der Stelle seines Auftretens bemerkt, sondern erst viel später. Die Zeilennummer bei einer Fehlermeldung wie »BLOCKSCHACHTELUNG: ANFANG FEHLT«, sagt also nichts weiter aus, als daß der Fehler erst hier bemerkt wurde. Um bei der Suche nach dem Fehler zu helfen, gibt Strubs aber während der Übersetzung ein Schema der Blockstruktur aus, mit dessen Hilfe sich solche Fehler leicht lokalisieren lassen. Bei Meldungen wie »zu viele Marken«, »zu viele WHILE/REPEAT« etc. empfiehlt es sich, das Programm in kleinere Teile zu zerlegen, diese getrennt zu übersetzen und anschließend wieder zusammenzufügen.

Wie man dazu vorgeht, behandeln wir weiter unten. Die entsprechenden Listen sind allerdings so großzügig dimensioniert, daß dieser Fall sehr selten eintreten wird.

Sollte während der Übersetzung aus irgendeinem Grund ein unkontrollierter Programmabbruch erfolgen (zum Beispiel OUT OF MEMORY ERROR), dann empfiehlt es sich mit »GOTO 5000« dafür zu sorgen, daß offene Disk-Files ordnungsgemäß geschlossen werden.

Die Beseitigung von Fehlern, die Strubs bei der Übersetzung entdeckt, gestaltet sich relativ einfach: Notieren Sie sich die Zeilennummern zu jedem Fehler und schalten in den Editbereich (mit »E«), Dort kann das Quellprogramm geändert werden, dann wird mit »!« und Wahl von »U« neu übersetzt. Da das Quellprogramm solange im Edit-Bereich bleibt, bis es durch »NEW« oder Laden eines anderen Programms gelöscht wird, kann dieser Vorgang solange wiederholt werden, bis der letzte Fehler beseitigt ist. Sobald die Übersetzung mit der Meldung »0 Fehler« beendet wird, geht es ans Testen des Objektprogramms.

Hierzu wird Strubs durch Eingabe von »S« verlassen. Dadurch wird ein Kaltstart ausgeführt, der die Interpretererweiterung abschaltet und den Speicher säubert. Nun laden Sie das Objektprogramm unter dem Namen, den Sie bei der Übersetzung angegeben haben und starten es mit RUN. Dieses Programm wird nun wie jedes normale Basic-Programm ausgetestet. Dazu können selbstverständlich auch Toolkits mit TRACE- und DUMP-Funktionen verwendet werden. Da die Zeilennummern denen des Quellprogramms entsprechen, empfiehlt es sich, ein Listing des Quellprogramms zur Hand zu haben. Fehler in der Programmlogik lassen sich damit leichter finden und beheben.

Die Bequemlichkeit, die Strubs dadurch bietet, daß Programmänderungen und Verbesserungen im Objektprogramm selbst vorgenommen und sofort ausgetestet werden können, erfordert auf der anderen Seite allerdings eine gewisse Disziplin, damit die Verbindung zum Quellprogramm nicht verloren geht. Jede vorgenommene Änderung sollte sorgfältig notiert und nicht zu viele Änderungen auf einmal vorgenommen werden. Dann wird wieder das Programm Strubs und das Quellprogramm (in den Editierbereich) geladen. Verbessern Sie das Quellprogramm entsprechend Ihren Notizen und übersetzen es erneut. Dieser Vorgang wird solange wiederholt, bis das Ergebnis zufriedenstellend ist.

Dieser soeben beschriebene Ablauf kann allerdings in den meisten Fällen vereinfacht werden: Bis auf zwei Ausnahmen können Objektprogramme auch direkt im Editbereich getestet werden. Damit entfällt die Notwendigkeit, Strubs für jede Übersetzung neu zu laden. Nach der Übersetzung wird mit »E« der Editbereich gewählt, dort das Objektprogramm geladen und getestet. Anschließend wird wieder das Quellprogramm in den Editbereich geladen, verbessert und mit »!« und »U« neu übersetzt und so weiter.

Bei Programmen, die nicht im Editbereich getestet werden können, handelt es sich 1. um Programme, die an einer festgelegten Stelle im Speicher stehen müssen. Strubs selbst ist solch ein Programm. Es muß unbedingt am normalen Basic-Anfang stehen. Solche Programme sind allerdings ziemlich selten. Häufiger dagegen findet sich der 2. Fall: Programme, die den Speicherbereich von 700 bis 800 verändern. Hier steht die in Folge 1 erwähnte Interpreter-Erweiterung von Strubs. Dadurch sind vor allem Programme betroffen, die in diesem Bereich Sprites oder Maschinenprogramme benutzen.

Kommen wir noch einmal auf das Schreiben und Editieren von Quellprogrammen zurück. Bisher haben wir nur davon gesprochen, daß die Programmtexte im Editbereich editiert wurden. Diese Methode hat insbesondere bei der Entwicklung umfangreicher Programme einen Nachteil: Da Strubs selbst mit einer Interpreter-Erweiterung arbeitet, kann man nicht gleichzeitig andere Interpreter-Erweiterungen — zum Beispiel Toolkits oder das DOS — benutzen. Möchte man auf Befehle wie MERGE, AUTO, FIND etc. nicht verzichten, dann kann man die Quellprogramme vollkommen unabhängig von Strubs entwickeln und erst anschließend das fertige Quellprogramm in den Editbereich laden.

Es zeigt sich, daß die meisten Beschränkungen bei der Arbeit mit Strubs ihren einzigen Grund in der kleinen Interpreter-Erweiterung haben. Wie bereits in der ersten Folge erwähnt, besteht der einzige Sinn dieser Erweiterung darin, das Starten von Strubs vom Editbereich aus dadurch bequemer zu gestalten, daß die Befehlsfolge

POKE 44,8: RUN

durch Eingabe von »!« abgekürzt werden kann. Wenn Sie bereit sind, diese Befehlsfolge jedesmal von Hand einzugeben, können Sie auf die Erweiterung verzichten, indem Sie im Programm die Zeilen 45600 bis 45680 einfach weglassen. Damit fallen dann die oben erwähnten Beschränkungen weg, das heißt die unter Fall 2 erwähnten Programme können im Editbereich getestet werden und Strubs kann zusammen mit einer Programmierhilfe benutzt werden. Aber editieren Sie keine Quellprogramme unter Simons Basic. Dazu sind weitere Anpassungen erforderlich, auf die wir in der nächsten Folge näher eingehen. Insbesondere wenn ein Programm aus fertigen Modulen zusammengesetzt werden soll, sind solche Programmierhilfen erforderlich.

Dieser Vorgang verläuft auf der Quellprogramm-Ebene aufgrund der Unabhängigkeit von Zeilennummern relativ einfach. Die einzelnen Programmteile werden in beliebiger Reihenfolge zusammengesetzt. Dazu kann ein Toolkit oder auch das kleine MERGE-Programm aus dem 64'er, Ausgabe 4/84, benutzt werden. Dabei können ruhig gleiche Zeilennummern auftreten und auch die Reihenfolge der Zeilennummern ist beliebig. Anschließend wird der fertige Programmtext mit Hilfe einer RENUMBER-Routine vernünftig durchnumeriert. Da Basic keine lokalen Variablen kennt, ist allerdings vor dem Zusammensetzen auf die Variablennamen zu achten. Um unerwünschte Seiteneffekte zu vermeiden, sind eventuell einige Umbenennungen vorzunehmen. Etwas aufwendiger gestaltet sich der Prozeß, wenn verschiedene Programmteile erst auf der Objekt-Ebene zusammengesetzt werden sollen. Hierbei ist darauf zu achten, daß sich die Bereiche der Zeilennummern nicht überschneiden. Weisen Sie jedem Programmteil einen bestimmten Zeilennummernbereich zu und verlegen diesen Teil gegebenenfalls mit RENUMBER in diesen Bereich. Anschließend werden nun in jedem Programmteil alle externen Routinen, die dieser Teil aufruft, mit Hilfe der EXTERN-Deklaration vereinbart (das sind die Routinen, die erst nach der Übersetzung angefügt werden). Das oben für die Variablennamen gesagte gilt hier entsprechend. Jetzt können die einzelnen Teile getrennt übersetzt und anschließend in der richtigen Reihenfolge verknüpft werden.

Falls Sie keine Erweiterung besitzen, dann können Sie Strubs um eine RENUMBER-Routine erweitern: Fügen Sie die Zeilen aus Bild 8 in das Objektprogramm von Strubs ein — und, falls Sie das Quellprogramm besitzen, dort entsprechend die Zeilen aus Bild 7. Um nun diese Routine anzubinden, müssen nur noch zwei Zeilen in das Menü eingefügt werden:

40110 PRINT "{CDOWN} {REV ON} R {REV OFF}ENUMBER"

und

40210 IF L$=”R” THEN GOSUB 55000: GOTO 40050

beziehungsweise für das Quellprogramm:

40210 IF Z$=”R” THEN GOSUB £RENUMBER: GOTO £MENUE

Diese Routine kann dann mit »R« aufgerufen werden, um ein Programm, das sich im Editbereich befindet, umzunumerieren. Das Programm Strubs arbeitet nicht mit der Datasette, sondern es benötigt eine Floppy. Damit der für Strubs unterhalb des Edit-Bereichs reservierte Platz nicht überschritten wird, ist darauf zu achten, daß beim Eintippen des Programms keine Blanks eingefügt werden.

Der Editbereich beginnt bei 40*256 + 1. Vor dem 1. Start von Strubs läßt sich mit ‘PEEK(46) feststellen, ob Strubs diese Grenze nicht überschreitet (der Wert muß kleiner als 40 sein, im Originalprogramm liegt er bei 34).

Da Strubs den Zeiger für »Variablen-Anfang« heraufsetzt, sollte es immer von dem 1. Start abgespeichert werden (auch bei Veränderungen). Sollte man dies einmal vergessen, kann man durch »POKE 46,39:CLR« vor den Abspeichern Strubs in die richtigen Grenzen verweisen. Die notwendigen Änderungen für VC 20 (mit 16 KByte) sind dem Bild 9 zu entnehmen. Hier beginnt der Editbereich bei 46*256+1.

40050 PRINT"{CLR}";"  *****************"
40052 PRINT "  * -- STRUBS  -- *"
40053 PRINT "  *  PRECOMPILER  *"
40055 PRINT "  * BITTE WAEHLEN *"
40058 PRINT "  *****************"
40060 PRINT"{DOWN}{DOWN}{DOWN}{RVON}E{RVOFF}DIT"


45600 I=0:READW
45610 POKE704+I,W:I=I+1:READW:IFW<256THEN45610
45620 DATA32,115,0,8,201,33,240,4,40,76,231,199
45630 DATA169,18,133,44,169,138,76,231,199,999
Bild 9. Diese Änderungen sind für die Anpassung von Strubs an den VC 20 (mit mindestens 16 KByte Erweiterung) erforderlich

Zusätzliche Funktionen

Die strukturierte Programmierung bietet vor allem Vorteile in bezug auf Wartung, Änderungen und Erweiterbarkeit von Programmen. Dies gilt auch für das Programm Strubs. Um in den Genuß dieser Vorteile zu gelangen, ist allerdings der Zugang zum Quellprogramm erforderlich. Wenn Sie sich das in Heft 5 abgedruckte Objektprogramm ansehen, werden Sie feststellen, daß es auch nicht viel aussagekräftiger als ein unkommentiertes Assemblerlisting ist. Wenn Sie an der Entwicklung eigener Programmerweiterungen interessiert sind, sollten Sie sich deshalb beim Verlag das Quellprogramm besorgen. Da ich hier davon ausgehen muß, daß die meisten Leser das Quellprogramm nicht besitzen, lohnt es sich gar nicht erst, systematisch die einzelnen Programmteile vorzustellen.

Statt dessen wollen wir nur die für Programmerweiterungen wichtigsten Programmelemente vorstellen und anhand einiger exemplarischer Erweiterungen, die auch, ohne sich weitere Gedanken zu machen, einfach eingetippt werden können, aufzeigen, wie man Erweiterungen implementieren kann und was dabei zu beachten ist. Aus dem gleichen Grund geben wir nur die Änderungen an, die im Objektprogramm vorzunehmen sind. Eine Anpassung an das Quellprogramm dürfte keine Probleme bereiten.

Achten Sie bei allen Programmänderungen darauf, daß das geänderte Programm abgespeichert wird, bevor es zum ersten Mal gestartet wird, da das Programm den Zeiger auf das Programmende verstellt. Sollte das Programm durch Erweiterungen so lang werden, daß es in den Editbereich hineinreicht, kann der Anfang des Editbereichs in Schritten zu 256 Byte nach oben verschoben werden, um Platz zu schaffen. Dazu ist in den Zeilen 70 bis 80 die Zahl 40 überall, wo sie auftaucht, durch eine größere Zahl (jeweils 4 für jedes Kilobyte) zu ersetzen (vgl. auch den Schluß der 3. Folge).

Die wichtigsten Programmelemente

Eine grobe Übersicht über den Aufbau des Programms haben wir bereits in der 2. Folge gegeben. Bevor wir uns nun mit einzelnen Erweiterungen beschäftigen, wollen wir zunächst einmal die wichtigsten Programmelemente vorstellen, die man für Änderungen und Erweiterungen des Programms benötigt. Wie bereits erwähnt, liest Strubs das Quellprogramm zweimal vom Anfang bis zum Ende durch. Um Zeit zu sparen, wird im 1. Lauf nur jeweils der Anfang einer Zeile untersucht. Deshalb müssen alle Befehle, die bereits im 1. Lauf zu behandeln sind, auch am Anfang einer Zeile stehen, während Befehle, die nur im 2. Lauf behandelt werden, überall stehen können. Ein Beispiel:

Die Definition von Marken muß am Zeilenanfang erfolgen, während der Aufruf von Marken an jeder Stelle erfolgen kann. Die Aufgabe des 1. Laufs besteht darin, verschiedene Tabellen anzulegen, mit deren Hilfe dann im 2. Lauf das endgültige Objektprogramm erzeugt wird.

Jede dieser Tabellen besteht aus einem oder mehreren Array(s), einer Variablen, deren zweiter Buchstabe ein »M« für »Maximal« ist und die Dimension, das heißt die maximale Zahl von Einträgen festlegt, und aus einer Variablen, deren zweiter Buchstabe ein »P« für »Pointer« ist und die auf den jeweils nächsten freien Listenplatz zeigt. Bei Speicherplatzproblemen brauchen nur die Werte der Dimensionsvariablen im Init-Teil geändert zu werden. Möchte man zum Beispiel mehr als 99 Marken (die jetzige Maximalzahl) benutzen, dann schreibt man in Zeile 45060 zum Beispiel »MM = 150:…«.

Die Tabellen werden in den Zeilen 45050 bis 45200 definiert (Bild 10). Die Dimension des Stacks bestimmt die mögliche Schachtelungstiefe. Dazu kommen die Tabellen der neuen Befehle (Zeile 45260 bis 45274) und der Fehlermeldungen (Zeile 45480 bis 45514).

Bild 10. Tabellen

Dem schrittweisen Lesen des Quellprogramms dienen die Variablen C und NC. Die Variable C enthält den Code des jeweils zuletzt gelesenen Zeichens, wobei der Wert 0 ein Zeilenende markiert. Die Variable NC enthält die Adresse des nächsten zu lesenden Zeichens.

Im 2. Lauf wird zeilenweise das Objektprogramm erzeugt, wobei die jeweils aktuelle Zeile in der Variablen Z$ aufgebaut wird. Dabei enthalten die beiden ersten Zeichen von Z$ Low- und Highbyte der Zeilennummer (so wie sie später im Speicher steht), und das letzte Zeichen der fertigen Zeile besteht aus dem Zeichen CHR$(0).

Die relevanten Zeichencodes, auf die Strubs reagiert, werden in den Zeilen 45240 bis 45254 definiert (Bild 11). Die Variable ZA enthält die Adresse des Anfangs der Zeile, die gerade bearbeitet wird. In EA steht die Startadresse des Editbereichs.

Bild 11. Relevante Zeichencodes

Damit kommen wir zu den für Erweiterungen wichtigen Modulen von Strubs. Die Prozedur »NEXT-CHAR« sucht ab Adresse NC das nächste relevante Zeichen des Quellprogrammtextes und liefert dessen Code in der Variablen C. Dabei werden Leerzeichen (Zeile 250) und Kommentare (Zeile 280-295) überlesen. Strings werden direkt in die Ausgabezeile Z$ übertragen (Zeile 350). Der Zeiger NC wird auf das nächste zu lesende Zeichen gesetzt. Die Prozedur »HOLNAME« (Zeile 750-830) liest ab aktueller Adresse NC einen Namen (zum Beispiel Befehl, Label) und zwar bis eines der Trennzeichen »:«, »,«, Blank oder Zeilenende erscheint. Der Name wird in der Variablen T$ ausgegeben, C enthält das erste relevante Zeichen hinter dem Namen (das ist außer beim Blank das Trennzeichen), und NC zeigt auf das nächste Zeichen.

Die Prozedur »SCHREIBZEILE« (Zeile 550-580) generiert auf der Diskette aus den nacheinander eingegebenen Zeilen Z$ das zusammenhängende Objektprogramm und gibt die Nummer der aktuellen Zeile auf dem Bildschirm aus. Die Variable AA (Linkadresse) darf außerhalb dieser Routine nicht verändert werden!

Die Prozedur »ERROR« (Zeile 8050 bis 8099) erwartet als Eingabe einen Fehlercode ER. Dabei handelt es sich um den Index der Fehlermeldung in der Tabelle der Fehlermeldungen. Die Zeilennummer und die Fehlermeldung werden auf dem Bildschirm ausgegeben und zugleich in eine Fehlertabelle eingetragen, die man sich nach der Übersetzung auf Bildschirm oder Drucker ausgeben lassen kann. Zusätzlich wird die Fehlermeldung in die Ausgabezeile Z$ geschrieben, so daß sie auch im Objektprogramm erscheint. Die Übersetzung wird mit der folgenden Zeile fortgesetzt.

Die Prozedur »ABBRUCH« (Zeile 50000 bis 50030) sorgt für einen kontrollierten Abbruch der Übersetzung. Sie erwartet ebenfalls als Eingabe den Fehlercode ER und gibt die entsprechende Fehlermeldung aus. Danach wird die Tabelle der bisher bemerkten Fehler ausgegeben, offene Files ordnungsgemäß geschlossen und Strubs neu gestartet.

Die Prozedur »WARTEN« (Zeile 49550 bis 49570) fordert den Benutzer auf, eine Taste zu drücken und wartet auf den Tastendruck.

Die Prozedur »INIT« (Zeile 45050 bis 45999) enthält die Definition der Variablen und Tabellen sowie die Interpretererweiterung.

Im »MENÜ« (Zeile 40050 bis 40495) können die verschiedenen Funktionen angewählt werden.

Die Prozeduren »BEFEHLE IM 1. LAUF« (Zeile 1550-2497) und »BEFEHLE IM 2. LAUF« (Zeile 2550-3640) werden von Strubs aufgerufen, sobald im Quellprogramm das Erkennungszeichen »!« für Befehle (Code in der Variablen BE) entdeckt wird. Sie holen den Namen des Befehls, suchen diesen in der Befehlstabelle und rufen entsprechend dem Index (+1) des Befehls in dieser Tabelle em Unterprogramm auf. Falls der Befehl nicht in der Tabelle gefunden wird, wird eine entsprechende Fehlermeldung ausgegeben. Im 1. Lauf kommt noch die Ausgabe der Blockstruktur hinzu. Hierzu dient die Variable In (für Indentmodus). IN = 0 bedeutet, auf der gleichen Schachtelungsebene zu bleiben.

Damit haben wir nun das notwendige Wissen zusammen, um an dem Programm Strubs einige Änderungen und Erweiterungen vorzunehmen.

Andere Anwendungen

Bei den Programmtexten, die Strubs übersetzt, handelt es sich zwar um erweiterte Basicprogramme, aber nichtsdestoweniger um Basicprogramme. Deshalb ist es relativ einfach, Strubs auch zur Bearbeitung ganz normaler Basic-Programme einzusetzen. Zwei sinnvolle Möglichkeiten wollen wir im folgenden vorstellen.

  1. Ein SPEED-UP-Programm, um normale Basicprogramme schneller zu machen.
  2. Ein Programm, das besser lesbare Listings erstellt.

Dabei ist zu beachten, daß die Änderungen, die wir dazu vornehmen, nicht wie die Makro-Funktion eine Erweiterung des eigentlichen Programmes Strubs und seiner Funktion darstellen, sondern daß wir zwei völlig neue Programme mit völlig neuen Aufgaben erhalten. Deshalb sollten auch die erhaltenen Programme unter neuen Namen, beispielsweise »SPEED-UP« und »LISTER«, abgespeichert werden. Das Arbeiten mit diesen Programmen unterscheidet sich nicht von der Arbeit mit dem »normalen« Strubs-Programm.

Schnellere Basic-Programme

Zunächst wollen wir Strubs so ändern, daß es normale Basicprogramme in Programme übersetzt, die keine Leerzeichen und Kommentare mehr enthalten und dadurch schneller ablaufen. Wie Sie sich erinnern werden, benutzt Strubs für Kommentare, die gelöscht werden sollen, ein eigenes Zeichen »'«. Kommentare, die mit REM gekennzeichnet werden, bleiben im Objektprogramm erhalten. Da Strubs bereits alle Blanks entfernt (außer in Strings), brauchen wir nur noch dafür zu sorgen, daß Strubs auf das REM-Token reagiert wie bisher auf das Kommentarzeichen »’«. Die relevanten Zeichencodes, auf die Strubs reagiert, werden in den Zeilen 45250 bis 45254 definiert (Bild 11). Wir brauchen nur in Zeile 45250 das KO = ASC(»’«) durch KO = 143 (143 ist das REM-Token) ersetzen und schon ist das Speed-Up-Programm fertig. Genauso können Sie die Erkennungszeichen für Label und die neuen Befehle ändern. Dies ist, um Konflikte zu vermeiden, für den Fall sinnvoll, daß Sie mit Strubs Programme für Interpretererweiterungen übersetzen, die ihrerseits »!« oder das Pfundzeichen als Erkennungszeichen für ihre neuen Befehle benutzen.

Listings

Haben Sie in Ihren Listings häufig Grafik- und Steuerzeichen? Dann können Sie sich viel Ärger ersparen, wenn Sie das Listing vorher mit dem Programm »LISTER« aufbereiten. »LISTER« übersetzt Basic-Programme in Programmtexte, in denen die schwer entzifferbaren Steuer- und Grafikzeichen innerhalb von Strings durch lesbare Worte »<CDOWN>« oder »<HOME>« ersetzt sind (Bild 12).

Bild 12. Beispiellister

Dazu ändern wir eine Zeile innerhalb der Prozedur »NEXTCHAR«. In Zeile 350 werden gelesene Zeichen mit dem ASCII-Code C innerhalb von Strings direkt in die Ausgabezeile Z$ übertragen. Wenn wir nun in Zeile 350 Z$=Z$+CHR$(C); durch Z$=Z$+C$(C); ersetzen, dann können wir ein Array C$(255) definieren, das in jedem ASCII-Wert den String enthält, der dafür im Objektprogramm erscheinen soll. Die Definition dieses Arrays gehört in das Modul »INITIALISIERUNG«:
45300 DIM C$(255):FOR I = 0 TO 255:C$(I) = CHR$(I):NEXT

Damit haben wir zugleich unser Array mit den normalen Werten vorbesetzt. Jetzt bleiben nur noch die Ersetzungen:
45310 C$(17)= "<CDOWN>":C$(19)="<HOME>"
45312 C$(28) = "<ROT>":C$(31)="<BLAU>"
… usw.

Hier können Sie nun jedem Zeichen ein beliebiges Wort zuordnen: Den ASCII-Code der einzelnen Zeichen finden Sie im C-64 Handbuch auf S. 135 oder Sie können ihn einfach durch Eingabe von
PRINT ASC("X")
feststellen, wobei »X« für das interessierende Zeichen steht. Bei sehr vielen Zeichen innerhalb eines Strings kann es allerdings vorkommen, daß die Zeilen zu lang werden. Deshalb sollten die Worte möglichst kurz gewählt werden.

Makros

An einem etwas umfangreicheren Beispiel wollen wir nun zeigen, wie man neue Strubs-Befehle implementiert und wie man die Prozeduren von Strubs benutzen kann. Dies soll am Beispiel einer Makro-Funktion demonstriert werden.

Makros, vor allem von Assemblern her bekannt, stellen so etwas wie Abkürzungen für kurze Programmausschnitte dar. Dadurch verringert sich die Tipparbeit und vor allem werden die Quellprogramme übersichtlicher.

In der Makro-Definition wird ein Makro-Name definiert und diesem ein Programmstück zugeordnet. Überall, wo nun im Quellprogramm ein Makro aufgerufen wird, erscheint im Objektprogramm an dieser Stelle das entsprechende Programmstück. Ein einmal definiertes Makro kann wie ein Label beliebig oft aufgerufen werden.

Für die Definition eines Makros wollen wir den Befehl »IDMAKRO« und für den Aufruf eines Makros den Befehl »!M« wählen. Ein Beispiel mag die Wirkungsweise der neuen Befehle demonstrieren:
10 !DMAKRO:NAME SYS 833:X= PEEK (878)

200 PRINT X:!M,NAME:PRINT X

Die Definitionszeile 10 wird gelöscht, da sie nur für die Übersetzung notwendige Informationen enthält. Die Zeile 200 mit dem Makro-Aufruf sieht im Objektprogramm folgendermaßen aus:
200 PRINTX:SYS833:X=PEEK(878): PRINTX

Einige Beispiele für Makros und deren korrekte Benutzung sowie das sich ergebende Objektprogramm zeigt Bild 13. Vor allem ist darauf zu achten, daß Makronamen wie alle Befehls- und Labelnamen mit einem der Trennzeichen abgeschlossen werden müssen. Insbesondere darf bei der Makrodefinition und beim Aufruf mit nachfolgenden Parametern (Spritemakros in Zeile 120 und 130) nicht das Blank hinter dem Makronamen vergessen werden! Jede Makrodefinition benötigt eine eigene Zeile. Eine Übergabe von Parametern an ein Makro ist nicht möglich. Achten Sie bei der Arbeit mit Makros darauf, daß die entstehenden Zeilen des Objektprogramms nicht zu lang werden. Zeilen, die länger als 80 Zeichen sind, lassen sich nicht mehr editieren. Zeilen, die länger als 256 Zeichen werden, führen zum unkontrollierten Abbruch der Übersetzung mit »String too long error«. In diesem Fall kann man mit »GOTO 50000« die Nummer der verantwortlichen Zeile erfahren und offene Files schließen.

Bild 13. Beispiele Makros

Um die Übersetzung zu ermöglichen, muß im 1. Lauf eine Tabelle der Makronamen und der zugehörigen Programmausschnitte angelegt werden. Im 2. Lauf werden dann alle Aufrufe durch den zugehörigen Text ersetzt. Die Verteilung auf zwei Läufe bietet den Vorteil, daß ein Makro (ebenso wie Labels) auch schon vor der Definition aufgerufen werden kann.

Zur Implementation sind folgende Schritte erforderlich: Zunächst muß dem Übersetzungsprogramm mitgeteilt werden, daß es zwei neue Befehle gibt. Dann müssen wir die notwendige Tabelle definieren und auch entsprechende Fehlermeldungen vorsehen. Diese Erweiterungen gehören in den INIT-Teil.

Schließlich muß noch dafür gesorgt werden, daß Strubs weiß, wie es im 1. und 2. Lauf auf die neuen Befehle zu reagieren hat. Die Befehlstabelle wird in Zeile 45265 definiert. Hier erhöhen wir die Zahl der Befehle um 2 und fügen dann noch eine DATA-Zeile mit den beiden neuen Befehlsnamen ein:
45265 BM = 15:…
45275 DATA DMAKRO,M

Wählt man Befehlsnamen, die reservierte Basic-Worte enthalten, dann müssen die Tokens berücksichtigt werden (wie dies für IF in der Zeile 45271 geschieht). Für einen Befehl »DEFMAKRO« wäre zum Beispiel
BE$(14) = CHR$(150) + "MAKRO” zu setzen (150=DEF-Token).

Für die Tabelle wählen wir ein Array NA$(NM,1), da der Name M bereits für die Markentabelle vergeben ist. Die Dimension (..,0) soll die Namen und die Dimension (..,1) den zugehörigen Text aufnehmen.
45155 NM = 40:DIM NA$(NM,1): NP = 0

Damit können 41 Makros definiert werden. Indem wir die Zahl der Fehlermeldungen von 9 auf 11 erhöhen, erhalten wir die beiden neuen Fehlercodes 10 und 11 für »zu viele Makros« und »undefiniertes Makro«.
45480 EM = ll:DIM…
45500 FOR I = 0 TO EM:READ …
45515 DATA "ZU VIELE MAKROS”, "UNDEFINIERTES MAKRO”

Nun müssen wir in die beiden Module »BEFEHLE IM 1. LAUF« beziehungsweise »BEFEHLE IM 2. LAUF« jeweils zwei Routinen für die neuen Befehle einfügen. Da die beiden Verteilerzeilen bereits voll sind, legen wir zwei neue Verteilerzeilen, an, die dann aber auch gleich für 10 weitere neue Befehle Platz bieten:
1571 IF I>14 THEN ON I-14 GOSUB 2350,2380
für den 1. Lauf und
2571 IF I>14 THEN ON I-14 GOSUB 3700,3750
für den 2. Lauf.

Die Routine für »IDMAKRO« im 1. Lauf soll zunächst prüfen, ob noch Platz in unserer Makro-Tabelle ist und, falls nicht, mit entsprechendem Fehlercode die Abbruch-Routine anspringen:
2350 IF NP>NM THEN ER = 10: GOTO 50000

Jetzt können wir mit Hilfe der Prozedur »HOLNAME« den Makro-Namen lesen und in unserer Tabelle speichern:
2355 Z$ = " ":GOSUB750:NA$(NP,0) =T$

Nun übertragen wir den Rest der Definitionszeile mit Hilfe von »NEXT-CHAR« nach Z$ (dadurch werden auch Strings mit übertragen. Als Ausgabezeile dient Z$ ja erst im 2. Lauf).
2360 Z$ = Z$ + CHR$(C);:GOSUB250:IFC<>0THEN 2360

Nun brauchen wir nur noch den Text in die Tabelle aufzunehmen, den Zeiger zu erhöhen und den Indentmodus angeben.
2370 NA$(NP,l) = Z$:NP = NP + l:IN = 0
2375 RETURN

Der Aufruf eines Makros interessiert im 1. Lauf nicht, also:
2380 IN=0:RETURN

Im 2. Lauf soll die Definitionszeile gelöscht werden. Dazu löschen wir den Ausgabestring und weisen C den Code für Zeilenende zu:
3700 Z$ = ””:C = 0:RETURN

Beim Aufruf eines Makros mit »!M« holen wir zunächst den Namen des Makros mit »HOLNAME« und suchen ihn in der Tabelle:
3750 GOSUB 750
3755 FOR I = 0 TO NP: IF NA$(I,0) <>T$ THEN NEXT

Falls der Name nicht gefunden wird, erfolgt ein Sprung zur Error-Routine mit dem Code für »undefiniertes Makro«:
3760 IF I>NP THEN ER=11: GOTO 8050

Nun ist nur noch das definierte Programmstück in die Ausgabezeile zu übertragen:
3760 Z$ = Z$ + NA$(I,l):RETURN

Dadurch, daß diese Makro-Erweiterung Zeile für Zeile besprochen wurde, um zu zeigen, wie man die von Strubs vorgegebenen Prozeduren benutzen kann, ist vielleicht der Eindruck entstanden, eine solche Erweiterung sei relativ kompliziert. Wenn Sie sich aber das Ganze noch einmal genauer ansehen, können Sie feststellen, daß für die Implementation neuer Befehle im Prinzip nur drei Schritte erforderlich sind:

  1. Eintrag der neuen Befehlsnamen in die Befehlstabelle
  2. Einfügen der entsprechenden Routinen
  3. Eintrag der Adressen dieser Routinen in die beiden Verteilerzeilen

Die ganze Arbeit des Suchens und Decodierens übernimmt Strubs automatisch.

Wie neue Funktionen (beispielsweise die Ausgabe der Makro-Tabelle) in das Menü aufgenommen werden können, haben sie bereits in der letzten Folge am Beispiel der RENUMBER-Funktion gesehen.

Eine Zusammenstellung der oben besprochenen Erweiterungen finden Sie in Bild 14.

Bild 14. Die besprochenen Erweiterungen auf einen Blick

Strubs und Interpretererweiterungen

Wollen Sie mit Strubs Programme für Interpretererweiterungen bearbeiten, dann sind einige weitere Dinge zu beachten. Entfernen Sie zunächst wie in Folge 3 beschrieben die Interpretererweiterung von Strubs.

Falls die Erweiterung, die Sie benutzen wollen, nicht in den Editor ingreift, sondern ihre neuen Befehle durch besondere Zeichen (meistens »!«) gekennzeichnet werden, dann ändern Sie wie bereits oben beschrieben die entsprechenden Erkennungszeichen, die Strubs benutzt.

Bei Erweiterungen wie Simon’s Basic, die in den Editor eingreifen und die neuen Befehle wie der Basic-Interpreter durch eigene Tokens darstellen, ist es am einfachsten, den Strubs-Befehlen, deren Namen solche Befehle enthalten, neue Namen zu geben. Im Fall von Simon’s Basic sind davon beispielsweise Strubs-Befehle wie »!REPEAT«, »!UNTIL« oder »!ELSE« etc. betroffen.

Dazu sind nur die Namen in den DATA-Zeilen 45272 bis 45274 zu ändern. Sie können die betroffenen Strubs-Befehle aber auch wie oben am Beispiel von »DEFMAKRO« beschrieben aus den Tokens zusammensetzen. Dabei ist aber zu berücksichtigen, daß die Tokens von Simon’s Basic aus zwei Zeichen und nicht wie die normalen Tokens aus nur einem Zeichen bestehen.

Eine Liste der von Strubs benutzten Variablen bietet Bild 15. Dabei kennzeichnet das Zeichen »*« Zeilennummern, in denen eine Wertzuweisung an die Variable erfolgt.

Bild 15. Variablenliste

Um sich an die Arbeitsweise von Strubs zu gewöhnen, können Sie das Quellprogramm »Menü« (Listing 2) eingeben. »Menü« faßt immer zehn Programme auf einer Bildschirmseite zusammen, die man mit den Cursor-Tasten durchblättern kann. Bei Programmen, die größer sind als »Menü«, muß in der ersten Zeile der Pointer auf das Basic-Ende korrigiert werden (sieheZeile 1 von »Menü«, Listing 3). Hängtman bei Programmen, die von »Menü« geladen werden sollen, an das Programmende LOAD”Menü.obj” ,8 dann wird bei Programmende »Menü« automatisch geladen und gestartet.

(Matthias Törk/og)
5 rem strubs4/4.9.83
10 '*******************************
15 '**  ---- strubs.4.qp ---     **
20 '** 4.9.83                    **
22 '** strubs.2  -code           **
25 '** basic prog voruebersetzen **
30 '** uebersetzt marken in zei- **
32 '** lennr. ( \name)           **
35 '** loescht kommentare '...'  **
36 '**         und blanks        **
40 '** befehle: mit '!'          **
41 '**         loop,exit,eloop   **
43 '**         if,else,fi        **
45 '**         caseof,of,ecase   **
46 '**         while,ewhile      **
47 '**         repeat,until      **
48 '**         ext:              **
49 '*******************************
50 '
51 print"{clr}";tab(10);"*****************"
52 print tab(10);"* --strubs.4 -- *"
55 print tab(10);"*   m.toerk     *"
57 print tab(10);"* 4352 herten   *"
58 print tab(10);"*****************"
67 '
68 '
70 !if peek(46)<40 or (peek(46)=40 and peek(45)<3)then'kein prog in editbereich
71 '
72 '   ** init edit u. var. bereich:
73 :   poke46,40:poke45,3:poke 40*256,0:clr
75 !fi
78 '
80 ea=40*256+1'  ** edit-bereich
100 gosub \init
140 goto \menue
148 '
149 '
200 '******************************
205 '**  -- next zeichen   ---   **
208 '** holt ab adr nc naechstes **
210 '** relevantes zeichen       **
212 '** ueberliest blanks und    **
214 '** kommentare zwischen      **
215 '** ' und  ' bzw zeilende    **
217 '** kopiert strings unveraen-**
218 '** dert nach z$             **
220 '** ein: nc   -char adr      **
222 '**      code-variablen      **
224 '** aus: nc   -adr next char **
226 '**      c    -char-code     **
228 '** sef: z$   -zeilenstring  **
247 '******************************
248 '
250 \nexchar:if peek(nc)=bl'ank'  then nc=nc+1:goto \this ' **blanks ueberlesen
254 '
260 c=peek(nc)
265 if c<>ko'mmentar' then \teststring
267 '
270 ' ** kommentar  ueberlesen
280 nc=nc+1:c=peek(nc):if c and c<>ko then \this
290 if c then nc=nc+1:c=peek(nc)
295 if c=bl then \nexchar
298 '
320 \teststring:  if c<>te'xt' then nc=nc+1:return
340 '
345 ' ** string nach z$ uebertragen  **
350 z$=z$+chr$(c):nc=nc+1:c=peek(nc):if c and c<>te'xt' then \this
370 nc=nc+1
390 return
395 '
500 '******************************
505 '** -schreib zeile auf disk- **
510 '** ein: z$ - zeilenstring   **
512 '** e/a: aa - linkadresse    **
513 '**      darf ausserhalb die-**
514 '**      ser routine nicht!! **
515 '**      veraendert werden ! **
520 '** sef: h%                  **
525 '** imp: fnad - adressfunkt. **
547 '******************************
548 '
550 \schreibzeile:if len(z$)<4 then return' **leerzeile
555 printfnad(za+2)
560 aa=aa+len(z$)+2 ' ** linkadr
565 h%=aa/256
570 print#1,chr$(aa-256*h%);chr$(h%);z$;
580 return
595 '
700 '*****************************
704 '** --- holname   ----      **
706 '** liest name ab adr nc    **
708 '** bis ":", ",", blank     **
709 '**     oder zeilenende     **
710 '** ein: nc                 **
715 '** aus: nc -adr. next char **
720 '**      c  -letztes gelese-**
722 '**         -nes zeichen    **
728 '**      t$ -name           **
747 '*****************************
748 '
750 \holname:t$=""
780 ' **** name lesen
790 !loop
795 :   c=peek(nc):if c=dp or c=km or c=bl or c=0    then !exit
800 :   nc=nc+1:t$=t$+chr$(c)
810 !eloop
820 nc=nc+1:if c=bl'ank' then gosub \nexchar
830 return
835 '
1000 '*****************************
1004 '** -- uebersetze marke --  **
1020 '** ein: z$ -zeilenanfang   **
1022 '**      nc -akt.char adr   **
1030 '** aus: z$ -z$+sprungziel  **
1032 '**      nc -auf letztes    **
1033 '**          gelesenes char **
1038 '** sef: i,h,t$             **
1047 '*****************************
1048 '
1050 \marke:gosub \holname
1115 '
1120 !if t$="this" then
1125 :    h=fnad(za+2)
1130 !else    '** marke suchen  ****
1140 :    for i=0 to mp:if ma$(i)<>t$ then next
1160 :    if i>mp then er=2:goto \error:'undefined label
1170 :    h=ma%(i)+di
1175 !fi
1180 z$=z$+mid$(str$(h),2)
1190 return
1195 '
1495 '
1500 '*********************************
1504 '** --- befehle im 1.lauf ----  **
1510 '** sef: sp,s%() stack          **
1530 '**      i%()    if/case tabelle**
1532 '**      lo%(,)  looptabelle    **
1533 '**      er,er%(),ep -errortab. **
1535 '**      i,in,ta,b$,h,l         **
1540 '** imp: holname,error,abbruch  **
1547 '*********************************
1549 '
1550 \befehl.l1:gosub \holname
1551 '
1560 for i=0 to bm:if t$<>be$(i) then next
1565 if i>bm then er=0:goto \error ' falscher befehl
1567 b$=be$(i):if i=3 then b$="if"
1568 '
1569 i=i+1  ' ** verteiler **
1570 onigosub\l1,\ex1,\el1,\if1,\els1,\fi1,\ca1,\of1,\ec1,\et1,\w1,\n1,\r1,\u1
1571 '
1572 '** blockstrucktur ausgeben **
1574 printfnad(za+2);
1575 if in'dentmodus'=0 then print tab(ta);b$:return
1577 if in=1 then print tab(ta);b$:ta=ta+1:return
1579 if in=2 then print tab(ta-1);b$:return
1581 if in=3 then ta=ta-1:print tab(ta);b$:return
1585 '
1586 return
1588 '
1589 ' ****  loop  *****
1600 \l1: if sp'tr'>sm'ax' then er=3:goto \abbruch
1605 :  if lp>lm then er=5:goto \abbruch
1609 '  * zeilennr merken:
1610 :  s'tack'%(sp)=lp:sp=sp+1:lo%(lp,0)=fnad(za+2)-di:lp=lp+1
1615 :  in'dentmodus'=1:return
1628 '
1629 ' **** eloop  *****
1640 \el1:sp=sp-1:if s'tack'p'ointer'<0 then er=1:goto \abbruch
1649 '  * zeilennummern zu entsprechendem loop nach lo%(,)
1650 :  lo'op'%(s%(sp),1)=fnad(za+2)-di
1660 :  in'dentmodus'=3:return
1678 '
1679 ' ** exit     *****
1680 \ex1: in'dentmodus'=0:return
1688 '
1689 '
1700 ' ** while    *****
1710 \w1: gosub \l1 'loop':return
1715 '
1730 ' *** ewhile ******
1740 \n1: gosub \el1 'eloop':return
1745 '
1800 ' ** repeat *******
1810 \r1: gosub \l1 'loop':return
1815 '
1845 '
1850 ' ** until  *******
1860 \u1: gosub \el1 'eloop':return
1948 '
1990 '
2000 ' ****  if  *******
2005 ' listenplatz fuer spaeteren sprungzieleintrag merken:
2010 \if1: if sp>sm then er=3:goto \abbruch
2011 :     if ip>im then er=4:goto \abbruch
2020 :     s%(sp)=ip:ip=ip+1:sp=sp+1
2025 :     in'dentmodus'=1:return
2029 '
2030 ' **** else *******
2035 ' zeilennr.+1 als sprungziel fuer zugehoeriges if eintragen:
2040 \els1:if sp<1 then er=1:goto \abbruch
2041 :   if ip>im then er=4:goto \abbruch
2044 :   i%(s%(sp-1))=fnad(za+2)+1-di
2045 '   * index fuer spaeteren sprungzieleintrag merken:
2050 :   s%(sp-1)=ip:ip=ip+1
2052 :   in'dentmodus'=2:return
2058 '
2090 ' ****  fi  *******
2095 ' znr. als sprungziel bei if bzw. else eintragen
2100 \fi1: if sp<1 then er=1:goto \abbruch
2105 :     sp=sp-1:i%(s%(sp))=fnad(za+2)-di
2107 :     in'dentmodus'=3:return
2108 '
2110 '
2150 ' **** caseof *****
2160 \ca1: if sp>sm then er=3:goto \abbruch
2165 :     s%(sp)=-1:sp=sp+1
2170 :     gosub \if1
2180 :     in'dentm.'=1:return
2185 '
2200 ' ***** of   ******
2210 \of1: gosub \els1
2230 :     gosub \if1
2240 :     in'dentm.'=2:return
2245 '
2250 ' ***** ecase *****
2260 \ec1: h=fnad(za+2)-di ' * zeilennr
2269 '  ** ausgaenge eintragen
2270 :  !loop
2275 :      if sp<1 then er=1:goto \abbruch
2280 :      sp=sp-1:i=s%(sp)
2290 :      if i<0 then !exit
2300 :      i%(i)=h
2310 :  !eloop
2320 :  in'dentm.'=3:return
2330 '
2399 ' *** ext/const ***
2400 \et1: !loop
2410 :     if mp>mm then er=6:goto \abbruch
2415 :     if c and c<>la'bel' then gosub \nexchar:goto \this
2420 :     if c then gosub \holname
2423 :     if c then gosub \nexchar
2425 :     if c<48 or c>57 then 'keine ziffer' er=9:goto \error
2430 :     ma$(mp)=t$:h=c
2438 '
2439 '     ** wert des labels: **
2440 :     gosub \holname
2450 :     ma%(mp)=val(chr$(h)+t$)-di
2460 :     mp=mp+1
2470 :     if c=0 then !exit
2480 !eloop
2481 '
2485 in'dentm.'=0:return
2495 '
2497 '
2500 '*********************************
2504 '** --- befehle im 2.lauf ----  **
2510 '** sef: stack                  **
2530 '**      ip,lp - tab. pointer   **
2534 '**      z$  - zeilenstring     **
2540 '** imp: holname                **
2547 '*********************************
2549 '
2550 \befehl.l2:gosub \holname
2551 '
2560 for i=0 to bm:if t$<>be$(i) then next
2565 if i>bm then er=0:goto \error ' * falscher befehl
2567 '
2568 i=i+1  ' ** verteiler **
2570 onigosub\l2,\ex2,\el2,\if2,\els2,\fi2,\ca2,\of2,\ec2,\et2,\w2,\n2,\r2,\u2
2575 return
2576 '
2589 ' ****  loop  *****
2590 \l2:if c=0 then z$=z$+":"
2592 '   index von loop/eloop paar merken
2595 :   s%(sp)=lp:sp=sp+1:lp=lp+1
2597 :   return
2628 '
2629 ' **** eloop  *****
2630 \el2: sp=sp-1
2639 '   * sprung zu entspr. loop
2640 :   z$=z$+g'o't'o'$+mid$(str$(lo%(s%(sp),0)+di),2)+nu$
2642 :   gosub \schreibzeile
2645 '   * folgezeile als sprungziel generieren
2647 :   l=peek(za+2)+1:h=peek(za+3):if l>255 then l=0:h=h+1
2648 :   z$=chr$(l)+chr$(h) +":"
2650 :   return
2652 '
2680 ' ****  exit  *****
2685 \ex2:b$="":if right$(z$,1)<>chr$(167) 'then-code' then b$=g'o't'o'$
2689 '   * sprung zu naechstem eloop
2693 :   z$=z$+b$+mid$(str$(lo%(s%(sp-1),1)+di+1),2)
2695 :   return
2947 '
2955 '
3000 ' ****  if  ********
3010 \if2: z$=z$+i'f'c$+no't'$+"("+chr$(c)
3020 :    gosub \nexchar:if c<>th'en' and c then z$=z$+chr$(c): goto \this
3030 :    z$=z$+")"+chr$(th'en')+mid$(str$(i%(ip)+di),2)
3035 '
3036 :    ip=ip+1:c=0:return
3039 '
3080 ' **** else ********
3090 \els2: z$=z$+g'o't'o'$+mid$(str$(i%(ip)+di),2)+nu$
3100 :    gosub \schreibzeile
3110 '   * folgezeile als sprungziel generieren:
3120 :   l=peek(za+2)+1:h=peek(za+3):if l>255 then l=0:h=h+1
3130 :   z$=chr$(l)+chr$(h) +":"
3140 :   ip=ip+1:return
3149 '
3180 ' ****  fi  ********
3190 \fi2: l=peek(za+2):h=peek(za+3)
3195 '   * zeile als sprungziel generieren:
3200 :   z$=chr$(l)+chr$(h) +":"
3210 :   return
3255 '
3259 ' ***** caseof ****
3260 \ca2: gosub \if2:return
3299 '
3300 ' ***** of   ******
3310 \of2: gosub \els2
3320 :     z$=left$(z$,len(z$)-1) ' ":" weg
3330 :     gosub \if2
3340 :     return
3345 '
3350 ' ***** ecase *****
3360 \ec2: gosub \fi2
3370 :  return
3380 '
3385 '
3399 ' *** ext/const ***
3400 \et2: z$="":c=0:return  ' *zeile loeschen
3405 '
3448 '
3449 ' *** while   *****
3450 \w2: gosub \l2 'loop'
3460 :    z$=z$+i'f'c'ode'$+no't'$+"("
3469 '    ** bedingung kopieren:
3470 :    if c<>be'fehl' and c then z$=z$+chr$(c):gosub \nexchar:goto \this
3480 :    z$=z$+")"+chr$(th'en')
3488 '    ** analog exit:
3490 :    z$=z$+mid$(str$(lo%(s%(sp-1),1)+di+1),2)
3495 :    c=0:return
3497 '
3498 '
3549 ' *** ewhile  *****
3550 \n2: gosub \el2 'eloop':return
3555 '
3557 '
3579 ' *** repeat  *****
3580 \r2: gosub \l2 'loop':return
3585 '
3599 ' *** until   *****
3600 \u2: z$=z$+i'f'c$+no't'$+"("
3605 '
3609 '   * bedingung kopieren
3610 :   if c then z$=z$+chr$(c):gosub \nexchar:goto \this
3619 '   * analog eloop
3620 :   sp=sp-1:in'dent'=3
3630 :   z$=z$+")"+chr$(th'en')+mid$(str$(lo%(s%(sp),0)+di),2)
3640 :   return
4000 '*****************************
4004 '** - bearbeite zeile  -    **
4020 '** ein: za -zeilenadr      **
4028 '** aus: z$ -zeilenstring   **
4029 '**          uebersetzte z. **
4035 '**      left$(z$,2)=zeilnr **
4040 '** imp: \befehl.l2         **
4045 '**      \marke             **
4047 '*****************************
4048 '
4050 ' ** zeilennr:       **
4060 \zeile:z$=chr$(peek(za+2))+chr$(peek(za+3))
4080 nc=za+4:gosub \nexchar ' 1.zeichen der zeile
4082 '
4089 ' **    'tabulator'  **
4090 if c=dp then gosub \nexchar
4098 '
4099 ' ** marke ueberlesen:  **
4100 !if c=la'bel' then
4105 :   gosub \holname:if c=dp then gosub \nexchar
4108 :   if c=0 then z$=z$+":"
4110 !fi
4111 '
4115 nc=nc-1:if c=0 then z$=z$+nu'll'$
4119 '
4120 ' ********  zeile lesen   ********
4130 !loop: if c=0 then !exit
4131 '
4132 :   gosub \nexchar
4138 '
4150 :   !if c=be'fehl' then
4155 :      gosub \befehl.l2
4358 :   !else
4360 :      if c=la'bel' then gosub \marke
4378 :   !fi
4380 :   z$=z$+chr$(c)
4395 ' ********  bis  zeilenende  *****
4396 !eloop
4398 return
4399 '
5000 '*****************************
5005 '** --- uebersetzen ---     **
5047 '*****************************
5048 '
5049 '
5050 \uebersetzen:  print"{clr}   ***** uebersetzen    ****{down}{down}{down}"
5052 !if fnad(ea)<ea+5 or fnad(ea)>ea+83 then
5053 :    print"kein programm vorhanden":gosub \warten:return
5054 !fi
5057 '
5058 print"bitte disk einlegen {down}{down} "
5059 '
5060 !loop  print"name fuer objekt-programm"
5065 :   poke198,1:poke631,34 ' **  " fuer input
5070 :   input f$
5080 :   open 1,8,1,f$+",p,w":open 15,8,15
5090 :   input#15,e,e$:if e=0 then !exit
5095 :   print"disk err:";e;e$
5096 :   input"neuer versuch";z$
5098 :   close1:close15
5099 :   if z$<>"j" then return
5100 !eloop
5118 '
5119 '
5120 aa=ea
5130 print#1,chr$(aa and 256);chr$(aa/256);'  ** startadr.
5134 '
5135 print"1.lauf"
5136 ta'bulator'=7 'fuer blockstruktur ausgabe
5140 gosub \1.lauf
5142 '" ** alle bloecke geschlossen?
5143 if sp>0 then print sp;:er=8:goto \abbruch
5144 '
5145 print"2.lauf"
5150 gosub \2.lauf
5154 '
5160 print#1,chr$(0);chr$(0);'  **** prog.ende marke
5180 close1:print"{down}**";ep;" errors **":gosub \warten
5190 return
5198 '
5199 '
5500 '*****************************
5504 '**  --- 1.lauf   ---       **
5510 '** imp: \nexchar           **
5512 '**      \mardef            **
5514 '**      \befehl.l1         **
5547 '*****************************
5548 '
5550 '  *** zeilenad.=editbereich anf
5555 \1.lauf:   za=ea
5557 '
5560 ' ** while nicht progr.ende do ***
5570 !while  za<>0  !do
5580 :   nc=za+4:c=peek(nc):nc=nc+1  '1.zeichen der zeile
5584 '   ** tab ueberlesen:
5585 :   if c=d'oppel'p'unkt' then gosub \nexchar
5587 '
5589 '   ** marke definieren
5590 :   if c=la'bel' then gosub \mardef:if c=dp then gosub \nexchar
5599 '
5619 '   ** befehl:
5620 :   if c=be'fehl' then gosub \befehl.l1
5920 :   za=fnad(za)
5930 !ewhile
5935 return
5940 ' **** endwhile ******************
5995 '
5996 '
6000 '*******************************
6004 '** --- marke definieren  --- **
6015 '** ein: za -zeilenadr.       **
6020 '** aus: veraenderte marken-  **
6022 '**      liste ma$(),ma%(),mp **
6030 '** sef: nc,t$                **
6047 '*******************************
6048 '
6050 \mardef:    if mp>mm'ax' then er=6:goto \abbruch
6070 gosub \holname
6095 '
6100 ma$(mp)=t$:ma%(mp)=fnad(za+2)-di:mp=mp+1
6120 return
6130 '
6500 '*****************************
6504 '** --- 2.lauf     ---      **
6510 '** imp: \zeile             **
6512 '**      \schreibzeile      **
6547 '*****************************
6548 '
6550 \2.lauf: z'eilen'a'dresse'=e'ditbereich'a'nfang':z1=fnad(za) 'adr. 2.zeile
6560 lp=0:sp=0:ip=0  ' * pointer ruecksetzen
6575 '
6580 !repeat
6585 :  !if peek(za+4)<>ko'mmentar' then
6590 :    gosub \zeile  ' bearbeiten
6600 :    gosub \schreibzeile
6649 '
6650 :  !fi
6655 :  za=z1:z1=fnad(z1) ' adresse naechste zeile
6660 !until z1=0
6670 ' * progr. ende *
6680 return
6685 '
8000 '*****************************
8004 '** --- error      -----    **
8047 '*****************************
8050 \error:print"error in";fnad(za+2),er$(er)
8060 if ep<em then er%(ep,0)=fnad(za+2)-di:er%(ep,1)=er:ep=ep+1
8080 z$=left$(z$,2)+"***** err:"+er$(er)+"********"
8090 c$=nu$:c=0  'zeilenende setzen
8099 return
8799 '
8800 '*****************************
8805 '** umschalten edit bereich **
8840 '** basic-anfang umsetzen   **
8847 '*****************************
8849 '
8850 '
8860 \edit:    print"{clr}{down}{down}{down}{down}{down}"
8870 printtab(9);"*********************"
8880 printtab(9);"** zurueck mit:    **"
8882 printtab(9);"** ' ! ' [return]  **"
8940 printtab(9);"*********************"
8950 poke44,ea/256:poke ea-1,0:clr:end
8990 end
40000 '****************************
40010 '**  --- menue ---         **
40048 '****************************
40049 '
40050 \menue:print"{clr}";tab(10);"*****************"
40052 print tab(10);"* -- strubs  -- *"
40053 print tab(10);"*  precompiler  *"
40055 print tab(10);"* bitte waehlen *"
40058 print tab(10);"*****************"
40060 print"{down}{down}{down}{rvon}e{rvof}dit"
40070 print"{down}{rvon}u{rvof}ebersetzen"
40080 print"{down}{rvon}m{rvof}arken-tabelle ausgeben"
40090 print"{down}{rvon}f{rvof}ehler-tabelle ausgeben"
40100 print"{down}{rvon}s{rvof}chluss"
40150 '
40160 get z$:if z$="" then \this
40170 if z$="e" then \edit
40180 if z$="u" then gosub \uebersetzen:goto \menue
40190 if z$="s" then sys 64738 '** kaltstart
40195 if z$="m" then gosub \markentab-aus:goto \menue
40200 if z$="f" then gosub \errortab-aus:goto \menue
40495 goto \menue
45000 '****************************
45010 '*  --- init  ---           *
45048 '****************************
45049 '
45050 ' ** marken-tabelle:
45060 \init: mm'ax'=99:dim ma$(mm),ma%(mm):mp=0
45069 '
45120 '
45130 ' ** loop-tabelle:
45131 ' *lo(..,0)=znr.loop
45132 ' *lo(..,1)=znr. zugehoeriges eloop
45135 l'oop'm'ax'=140:dim lo'op'%(lm,1):l'oop'p'ointer'=0
45138 '
45140 ' ** if-tabelle:
45145 im'ax'=270:dim i%(im):ip=0
45149 '
45188 '
45189 ' ** stack:
45190 sm'ax'=60:dim s'tack'%(sm):sp'tr'=0
45200 '
45209 '
45210 ' ** differenz fuer zeilennr. in integer-array
45220 di=32766
45225 '
45240 ' ** relevante zeichencodes **
45250 dp=asc(":"):ko'mmentar'=asc("'"):la'bel'=asc("\"):nu$=chr$(0):bl=asc(" ")
45253 be'fehl'=asc("!"):te'xt("")'=34:g'o't'o-code'$=chr$(137)
45254 i'f'c'ode'$=chr$(139):th'en-code'=167:no't'$=chr$(168):k'om'm'a-code'=44
45259 '
45260 '***** befehle:  ****************
45265 bm=13:dim be$(bm)
45270 for i=0 to bm:read be$(i):next
45271 be$(3)=i'f'c'ode'$
45272 data loop,exit,eloop,if,else,fi
45273 data caseof,of,ecase,ext
45274 data while,ewhile,repeat,until
45399 '
45400 ' ** adressberechnung:
45410 def fnad(x)=peek(x)+256*peek(x+1)
45412 '
45415 '
45470 ' ** error-tabelle:
45480 em=40:dim er%(em,1):ep=0:dim er$(40)
45490 ' ** fehlermeldungen
45500 fori=0to9:read er$(i):next
45510 data "falscher befehl","blockschachtelung: anfang fehlt"
45511 data "undefinierte marke","stack voll"
45512 data "zu viele if/else/case/of","zu viele loop/while/repeat"
45513 data "zu viele marken",,"block nicht geschlossen"
45514 data "extern declaration"
45595 '
45599 ' ** interpretererw. '!' = poke44,8:run
45600 i=0:read w
45610 poke 704+i,w:i=i+1:read w:if w<256 then \this
45620 data 32,115,0,8,201,33,240,4,40,76,231,167
45630 data 169,8,133,44,169,138,76,231,167,999
45640 ' * umschalten:
45650 for i=0 to 10:read w:poke 750+i,w
45660 next
45670 sys 750
45680 data 169,192,141,8,3,169,2,141,9,3,96
45690 '
45999 return
48000 '********************************
48003 '** - markentabelle ausgeben - **
48048 '********************************
48049 '
48050 \markentab-aus:if mp=0 then return
48055 h=0 ' flag
48057 print"{clr}{down}      ** markentabelle ausgeben **"
48060 input"{down}{down} auf drucker (j/n)";b$
48070 !if b$="j" then
48075 :   print" drucker an?":gosub \warten
48080 :   open 1,4
48090 !else
48100 :   open 1,3 'bildschirm
48102 :   h=-1 ' flag
48104 !fi
48105 '
48120 for i=0 to mp-1
48140 :   print#1,ma%(i)+di,ma$(i)
48150 :   if i-int(i/10)*10 =0 then if i and h then gosub \warten
48180 next
48185 close1:gosub \warten
48190 return
48195 '
49000 '********************************
49003 '** - fehlertabelle ausgeben - **
49048 '********************************
49049 '
49050 \errortab-aus:if ep=0 then return
49055 h=0 ' flag
49057 print"{clr}{down}      ** fehlertabelle ausgeben **"
49060 input"{down}{down} auf drucker (j/n)";b$
49070 !if b$="j" then
49075 :   print"{down} drucker an?{down}{down}":gosub \warten
49080 :   open 1,4
49090 !else
49100 :   open 1,3 'bildschirm
49102 :   h=-1 ' flag
49104 !fi
49105 '
49110 print#1,ep;" errors"
49120 for i=0 to ep-1
49140 :   print#1, er%(i,0)+di;er$(er%(i,1))
49150 :   if i-int(i/10)*10 =0 then if i and h then gosub \warten
49180 next
49185 close1
49191 gosub \warten
49190 return
49195 '
49500 '********************************
49503 '** --- auf taste warten   --- **
49548 '********************************
49549 '
49550 \warten:print"->{left}{left}";
49560 getb$:if b$="" then \this
49570 return
49598 '
49599 '
49950 '********************************
49955 '** --- progr.abbruch      --- **
49958 '** schliesst file             **
49970 '** gibt fehlermeldung aus     **
49975 '** ein: er -fehlercode        **
49990 '********************************
50000 \abbruch: print "{down}* fehler beheben, dann neu versuchen *"
50008 print:print er$(er);" in ";fnad(za+2)
50010 print#1,chr$(0);chr$(0); '  **** prog.ende marke
50020 close1
50030 gosub \warten
50040 gosub \errortab-aus
50050 run
Listing 1. Das Objektprogramm Strubs. Bitte beachten Sie die Eingabehinweise auf Seite 16.
5 remstrubs4/4.9.83
51 print"{clr}";tab(10);"*****************"
52 printtab(10);"* --strubs.4 -- *"
55 printtab(10);"*   m.toerk     *"
57 printtab(10);"* 4352 herten   *"
58 printtab(10);"*****************"
70 ifnot(peek(46)<40or(peek(46)=40andpeek(45)<3))then75
73 poke46,40:poke45,3:poke40*256,0:clr
75 :
80 ea=40*256+1
100 gosub45060
140 goto40050
250 ifpeek(nc)=blthennc=nc+1:goto250
260 c=peek(nc)
265 ifc<>kothen320
280 nc=nc+1:c=peek(nc):ifcandc<>kothen280
290 ifcthennc=nc+1:c=peek(nc)
295 ifc=blthen250
320 ifc<>tethennc=nc+1:return
350 z$=z$+chr$(c):nc=nc+1:c=peek(nc):ifcandc<>tethen350
370 nc=nc+1
390 return
550 iflen(z$)<4thenreturn
555 printfnad(za+2)
560 aa=aa+len(z$)+2
565 h%=aa/256
570 print#1,chr$(aa-256*h%);chr$(h%);z$;
580 return
750 t$=""
790 :
795 c=peek(nc):ifc=dporc=kmorc=blorc=0then811
800 nc=nc+1:t$=t$+chr$(c)
810 goto790
811 :
820 nc=nc+1:ifc=blthengosub250
830 return
1050 gosub750
1120 ifnot(t$="this")then1131
1125 h=fnad(za+2)
1130 goto1175
1131 :
1140 fori=0tomp:ifma$(i)<>t$thennext
1160 ifi>mpthener=2:goto8050:
1170 h=ma%(i)+di
1175 :
1180 z$=z$+mid$(str$(h),2)
1190 return
1550 gosub750
1560 fori=0tobm:ift$<>be$(i)thennext
1565 ifi>bmthener=0:goto8050
1567 b$=be$(i):ifi=3thenb$="if"
1569 i=i+1
1570 onigosub1600,1680,1640,2010,2040,2100,2160,2210,2260,2400,1710,1740,1810,1860
1574 printfnad(za+2);
1575 ifin=0thenprinttab(ta);b$:return
1577 ifin=1thenprinttab(ta);b$:ta=ta+1:return
1579 ifin=2thenprinttab(ta-1);b$:return
1581 ifin=3thenta=ta-1:printtab(ta);b$:return
1586 return
1600 ifsp>smthener=3:goto50000
1605 iflp>lmthener=5:goto50000
1610 s%(sp)=lp:sp=sp+1:lo%(lp,0)=fnad(za+2)-di:lp=lp+1
1615 in=1:return
1640 sp=sp-1:ifsp<0thener=1:goto50000
1650 lo%(s%(sp),1)=fnad(za+2)-di
1660 in=3:return
1680 in=0:return
1710 gosub1600:return
1740 gosub1640:return
1810 gosub1600:return
1860 gosub1640:return
2010 ifsp>smthener=3:goto50000
2011 ifip>imthener=4:goto50000
2020 s%(sp)=ip:ip=ip+1:sp=sp+1
2025 in=1:return
2040 ifsp<1thener=1:goto50000
2041 ifip>imthener=4:goto50000
2044 i%(s%(sp-1))=fnad(za+2)+1-di
2050 s%(sp-1)=ip:ip=ip+1
2052 in=2:return
2100 ifsp<1thener=1:goto50000
2105 sp=sp-1:i%(s%(sp))=fnad(za+2)-di
2107 in=3:return
2160 ifsp>smthener=3:goto50000
2165 s%(sp)=-1:sp=sp+1
2170 gosub2010
2180 in=1:return
2210 gosub2040
2230 gosub2010
2240 in=2:return
2260 h=fnad(za+2)-di
2270 :
2275 ifsp<1thener=1:goto50000
2280 sp=sp-1:i=s%(sp)
2290 ifi<0then2311
2300 i%(i)=h
2310 goto2270
2311 :
2320 in=3:return
2400 :
2410 ifmp>mmthener=6:goto50000
2415 ifcandc<>lathengosub250:goto2415
2420 ifcthengosub750
2423 ifcthengosub250
2425 ifc<48orc>57thener=9:goto8050
2430 ma$(mp)=t$:h=c
2440 gosub750
2450 ma%(mp)=val(chr$(h)+t$)-di
2460 mp=mp+1
2470 ifc=0then2481
2480 goto2400
2481 :
2485 in=0:return
2550 gosub750
2560 fori=0tobm:ift$<>be$(i)thennext
2565 ifi>bmthener=0:goto8050
2568 i=i+1
2570 onigosub2590,2685,2630,3010,3090,3190,3260,3310,3360,3400,3450,3550,3580,3600
2575 return
2590 ifc=0thenz$=z$+":"
2595 s%(sp)=lp:sp=sp+1:lp=lp+1
2597 return
2630 sp=sp-1
2640 z$=z$+gt$+mid$(str$(lo%(s%(sp),0)+di),2)+nu$
2642 gosub550
2647 l=peek(za+2)+1:h=peek(za+3):ifl>255thenl=0:h=h+1
2648 z$=chr$(l)+chr$(h)+":"
2650 return
2685 b$="":ifright$(z$,1)<>chr$(167)thenb$=gt$
2693 z$=z$+b$+mid$(str$(lo%(s%(sp-1),1)+di+1),2)
2695 return
3010 z$=z$+ic$+no$+"("+chr$(c)
3020 gosub250:ifc<>thandcthenz$=z$+chr$(c):goto3020
3030 z$=z$+")"+chr$(th)+mid$(str$(i%(ip)+di),2)
3036 ip=ip+1:c=0:return
3090 z$=z$+gt$+mid$(str$(i%(ip)+di),2)+nu$
3100 gosub550
3120 l=peek(za+2)+1:h=peek(za+3):ifl>255thenl=0:h=h+1
3130 z$=chr$(l)+chr$(h)+":"
3140 ip=ip+1:return
3190 l=peek(za+2):h=peek(za+3)
3200 z$=chr$(l)+chr$(h)+":"
3210 return
3260 gosub3010:return
3310 gosub3090
3320 z$=left$(z$,len(z$)-1)
3330 gosub3010
3340 return
3360 gosub3190
3370 return
3400 z$="":c=0:return
3450 gosub2590
3460 z$=z$+ic$+no$+"("
3470 ifc<>beandcthenz$=z$+chr$(c):gosub250:goto3470
3480 z$=z$+")"+chr$(th)
3490 z$=z$+mid$(str$(lo%(s%(sp-1),1)+di+1),2)
3495 c=0:return
3550 gosub2630:return
3580 gosub2590:return
3600 z$=z$+ic$+no$+"("
3610 ifcthenz$=z$+chr$(c):gosub250:goto3610
3620 sp=sp-1:in=3
3630 z$=z$+")"+chr$(th)+mid$(str$(lo%(s%(sp),0)+di),2)
3640 return
4060 z$=chr$(peek(za+2))+chr$(peek(za+3))
4080 nc=za+4:gosub250
4090 ifc=dpthengosub250
4100 ifnot(c=la)then4110
4105 gosub750:ifc=dpthengosub250
4108 ifc=0thenz$=z$+":"
4110 :
4115 nc=nc-1:ifc=0thenz$=z$+nu$
4130 :ifc=0then4397
4132 gosub250
4150 ifnot(c=be)then4359
4155 gosub2550
4358 goto4378
4359 :
4360 ifc=lathengosub1050
4378 :
4380 z$=z$+chr$(c)
4396 goto4130
4397 :
4398 return
5050 print"{clr}   ***** uebersetzen    ****{down}{down}{down}"
5052 ifnot(fnad(ea)<ea+5orfnad(ea)>ea+83)then5054
5053 print"kein programm vorhanden":gosub49550:return
5054 :
5058 print"bitte disk einlegen {down}{down} "
5060 print"name fuer objekt-programm"
5065 poke198,1:poke631,34
5070 inputf$
5080 open1,8,1,f$+",p,w":open15,8,15
5090 input#15,e,e$:ife=0then5101
5095 print"disk err:";e;e$
5096 input"neuer versuch";z$
5098 close1:close15
5099 ifz$<>"j"thenreturn
5100 goto5060
5101 :
5120 aa=ea
5130 print#1,chr$(aaand256);chr$(aa/256);
5135 print"1.lauf"
5136 ta=7
5140 gosub5555
5143 ifsp>0thenprintsp;:er=8:goto50000
5145 print"2.lauf"
5150 gosub6550
5160 print#1,chr$(0);chr$(0);
5180 close1:print"{down}**";ep;" errors **":gosub49550
5190 return
5555 za=ea
5570 ifnot(za<>0)then5931
5580 nc=za+4:c=peek(nc):nc=nc+1
5585 ifc=dpthengosub250
5590 ifc=lathengosub6050:ifc=dpthengosub250
5620 ifc=bethengosub1550
5920 za=fnad(za)
5930 goto5570
5931 :
5935 return
6050 ifmp>mmthener=6:goto50000
6070 gosub750
6100 ma$(mp)=t$:ma%(mp)=fnad(za+2)-di:mp=mp+1
6120 return
6550 za=ea:z1=fnad(za)
6560 lp=0:sp=0:ip=0
6580 :
6585 ifnot(peek(za+4)<>ko)then6650
6590 gosub4060
6600 gosub550
6650 :
6655 za=z1:z1=fnad(z1)
6660 ifnot(z1=0)then6580
6680 return
8050 print"error in";fnad(za+2),er$(er)
8060 ifep<emthener%(ep,0)=fnad(za+2)-di:er%(ep,1)=er:ep=ep+1
8080 z$=left$(z$,2)+"***** err:"+er$(er)+"********"
8090 c$=nu$:c=0
8099 return
8860 print"{clr}{down}{down}{down}{down}{down}"
8870 printtab(9);"*********************"
8880 printtab(9);"** zurueck mit:    **"
8882 printtab(9);"** ' ! ' [return]  **"
8940 printtab(9);"*********************"
8950 poke44,ea/256:pokeea-1,0:clr:end
8990 end
40050 print"{clr}";tab(10);"*****************"
40052 printtab(10);"* -- strubs  -- *"
40053 printtab(10);"*  precompiler  *"
40055 printtab(10);"* bitte waehlen *"
40058 printtab(10);"*****************"
40060 print"{down}{down}{down}{rvon}e{rvof}dit"
40070 print"{down}{rvon}u{rvof}ebersetzen"
40080 print"{down}{rvon}m{rvof}arken-tabelle ausgeben"
40090 print"{down}{rvon}f{rvof}ehler-tabelle ausgeben"
40100 print"{down}{rvon}s{rvof}chluss"
40160 getz$:ifz$=""then40160
40170 ifz$="e"then8860
40180 ifz$="u"thengosub5050:goto40050
40190 ifz$="s"thensys64738
40195 ifz$="m"thengosub48050:goto40050
40200 ifz$="f"thengosub49050:goto40050
40495 goto40050
45060 mm=99:dimma$(mm),ma%(mm):mp=0
45135 lm=140:dimlo%(lm,1):lp=0
45145 im=270:dimi%(im):ip=0
45190 sm=60:dims%(sm):sp=0
45220 di=32766
45250 dp=asc(":"):ko=asc("'"):la=asc("\"):nu$=chr$(0):bl=asc(" ")
45253 be=asc("!"):te=34:gt$=chr$(137)
45254 ic$=chr$(139):th=167:no$=chr$(168):km=44
45265 bm=13:dimbe$(bm)
45270 fori=0tobm:readbe$(i):next
45271 be$(3)=ic$
45272 dataloop,exit,eloop,if,else,fi
45273 datacaseof,of,ecase,ext
45274 datawhile,ewhile,repeat,until
45410 deffnad(x)=peek(x)+256*peek(x+1)
45480 em=40:dimer%(em,1):ep=0:dimer$(40)
45500 fori=0to9:reader$(i):next
45510 data"falscher befehl","blockschachtelung: anfang fehlt"
45511 data"undefinierte marke","stack voll"
45512 data"zu viele if/else/case/of","zu viele loop/while/repeat"
45513 data"zu viele marken",,"block nicht geschlossen"
45514 data"extern declaration"
45600 i=0:readw
45610 poke704+i,w:i=i+1:readw:ifw<256then45610
45620 data32,115,0,8,201,33,240,4,40,76,231,167
45630 data169,8,133,44,169,138,76,231,167,999
45650 fori=0to10:readw:poke750+i,w
45660 next
45670 sys750
45680 data169,192,141,8,3,169,2,141,9,3,96
45999 return
48050 ifmp=0thenreturn
48055 h=0
48057 print"{clr}{down}      ** markentabelle ausgeben **"
48060 input"{down}{down} auf drucker (j/n)";b$
48070 ifnot(b$="j")then48091
48075 print" drucker an?":gosub49550
48080 open1,4
48090 goto48104
48091 :
48100 open1,3
48102 h=-1
48104 :
48120 fori=0tomp-1
48140 print#1,ma%(i)+di,ma$(i)
48150 ifi-int(i/10)*10=0thenifiandhthengosub49550
48180 next
48185 close1:gosub49550
48190 return
49050 ifep=0thenreturn
49055 h=0
49057 print"{clr}{down}      ** fehlertabelle ausgeben **"
49060 input"{down}{down} auf drucker (j/n)";b$
49070 ifnot(b$="j")then49091
49075 print"{down} drucker an?{down}{down}":gosub49550
49080 open1,4
49090 goto49104
49091 :
49100 open1,3
49102 h=-1
49104 :
49110 print#1,ep;" errors"
49120 fori=0toep-1
49140 print#1,er%(i,0)+di;er$(er%(i,1))
49150 ifi-int(i/10)*10=0thenifiandhthengosub49550
49180 next
49185 close1
49191 gosub49550
49190 return
49550 print"->{left}{left}";
49560 getb$:ifb$=""then49560
49570 return
50000 print"{down}* fehler beheben, dann neu versuchen *"
50008 print:printer$(er);" in ";fnad(za+2)
50010 print#1,chr$(0);chr$(0);
50020 close1
50030 gosub49550
50040 gosub49050
50050 run
Listing 2. »Menue«, das erste Strubs-Listing
Listing 3. Das Objektprogramm »Menue«
PDF Diesen Artikel als PDF herunterladen
Mastodon Diesen Artikel auf Mastodon teilen
← Vorheriger ArtikelNächster Artikel →