Compiler-Optionen des GFA-BASIC-Compilers

Legt man Wert auf absturzsichere Programme, und/ oder möchte man die neuen Interrupt-Möglichkeiten des GFA-BASIC 3.x nutzen, muß ein Compiler-Lauf mittels Software-Schalter (Optionen) gewissenhaft gesteuert werden, sonst laufen die Programme nicht einwandfrei. Mit dem Handbuch allein tut man sich jedoch oft schwer.

Der Compiler des GFA-BASIC V3.x reagiert auf verschiedene Software-Schalter, die vom BASIC-Programmierer im GFA-BASIC-Sourcecode mittels Einfügung von z.B. ‘$E+’ geschaltet werden können. Der Interpreter überliest bei der Programmausführung diese Umschaltungen, im Compiler sollen die Schalter in der Hauptsache dazu dienen, dem Programmierer die Entscheidung zwischen kurzem, schnellem oder sicherem Code zu überlassen.

Das Handbuch sorgt durch Beschreibung der Schalter (bzw. Optionen) sowie durch Beispielprogramme dafür, sie effektiv und vor allem richtig anwenden zu können. Leider wird dabei aber versäumt, auf die Optionen $U, $P und $I im Zusammenhang mit den Interrupt-Befehlen ‘EVERY xxx GOSUB proc’ und der Fehlerbehandlung durch ‘ON ERROR GOSUB proc’ genügend tief einzugehen.

Bekanntlich führen sämtliche BASIC-Fehlermeldungen - z.B. auch ein ganz banales ‘Diskette voll’ - zum Programmabbruch, sofern nicht explizit eine Fehlerbehandlung durchgeführt wird. Man sieht, Fehlerbehandlung ist kein Luxus-Feature für Leute, die es besonders schön machen wollen.

Wichtig ist das gründliche Wissen um die Compiler-Optionen deshalb, weil das Nichtsetzen bzw. falsch Setzen nicht etwa nur einen etwas längeren oder langsameren Code zur Folge hat, das Programm wird vielmehr mit dem Auftreten bestimmter Situationen (Timing, Fehler) seine eigenen Wege gehen. Busfehler weit entfernt von der Ursache des eigentlichen Fehlers (zeitlich wie örtlich) als auch mysteriöse rekursive Prozeduraufrufe grüßen dann mit einem fröhlichen „viel Spaß beim Entwanzen“.

Es folgt die Beschreibung der Optionen. Ein praxisorientiertes Kochrezept, wann welche Schalter gesetzt bzw. nicht gesetzt sein müssen, folgt am Ende.

Schalter $P...

... wirkt sich beim Einstieg in eine Prozedur und beim Verlassen selbiger aus. Mögliche Schalterstellungen sind $P> und $P<. Bei Prozeduraufrufen muß sich die aufgerufene Routine die Programmstelle merken, die nach dem RETURN als nächstes bearbeitet werden soll. Mit $P< entscheidet man sich für die Hinterlegung dieser Adresse auf dem Stack. Natürlich muß dann während der gesamten Routine der Stackpointer (Prozessorregister a7) streng behütet werden. Mit $P> wählt man die Ablage auf dem Heap. Es gilt im Grunde das gleiche wie eben, der Heappointer (Prozessorregister a3) darf nicht verlorengehen.

Der Heappointer kann während des Programmlaufs auf Kosten von Ausführungsgeschwindigkeit und Codelänge wesentlich sorgsamer behandelt werden als der Stackpointer. Tritt während einer Bibliotheksfunktion ein Fehler auf (Division durch Null, Überlauf), gerät der Stackpointer meist außer Kontrolle. Dies läßt man sich allerdings gefallen, weil der Stack vom Prozessor ständig benutzt wird und deshalb das Sichern des Stackpointers massig Zeit und Code kosten würde. In 99.99% der Fälle wären solche pauschalen Vorsorgemaßnahmen sowieso für die Katz.

Die Compiler-Philosophie muß deshalb sein, nur bei Bedarf die Ablage aller lebensnotwendigen Informationen zu veranlassen, mit denen sich das Programm im Fehlerfalle wieder in geregelte Bahnen lenken läßt.

Hier tritt der Grund für die Einbeziehung des Programmierers in den Compi-liervorgang deutlich zutage: Er kennt die fehlerträchtigen Programmstellen und erhält deshalb die Erlaubnis, das Sichern dieser Informationen gezielt ‘von Hand’ einzufügen - das Programm läuft dann zeitweise (oder ganz) mit Netz und doppeltem Boden.

Die P-Option stellt eine dieser Programmiererfreiheiten dar. Nebenbei: Falls in der Prozedur lokale Variablen benutzt werden, ‘overruled- der Compiler den Programmierer und benutzt immer die $P>-Version.

Schalter $U...

... ist wichtiger als die P-Option, da sie zwischen allen codeerzeugenden BASIC-Befehlen eingesetzt werden kann. Mit $U+ veranlaßt man die wiederholte Einfügung des Maschinenbefehls ‘jsr(a6)’ (jsr=jump subroutine = bearbeite Unterprogramm) vor jedem BASIC-Befehl bis zum Abschalten der Option durch $U-. Möchte man nur einzelne ‘jsr (a6)’ einfügen, verhindert man die automatische Einfügung mit ‘$U-’ und schreibt gezielt ‘$U’ an den relevanten Stellen.

Der Compiler erzeugt eine Tabelle im Datenbereich des Programms, die über die Adressen aller ‘jsr (a6)’ im Programm Buch führt. Jeder Eintrag in dieser Tabelle bezeichnet dabei den Adreßabstand zum Vorgänger - $U als vorzeichenloses Wort (erster Eintrag = Programmstart bis erstes $U). Die Tabelle kommt bei Abarbeitung der ‘RESUME NEXT’-Anweisung zum Einsatz.

Zwischen den Zeilen

Was leistet nun das kleine Unterprogramm ‘jsr (a6)’? Die Prozessorregister Stackpointer (a7), Heappointer (a3) und Programmzähler (pc) werden in einen speziellen Speicherbereich kopiert und damit die Einträge des letzten Aufrufs überschrieben. Der genannte pc enthält die Adresse des Maschinenbefehls nach ‘jsr (a6)’, also die Anfangsadresse der dem $U folgenden BASIC-Zeile.

Ist dies erledigt, kehrt die Routine sofort zurück, es sei denn, es wurde die Interrupt-Option $I+ eingeschaltet. Dann wird die Tastenkombination Alt-Shift-Ctrl getestet und, falls eine EVERY ...- bzw. AFTER ...-Routine aktiviert ist, die Zeit geprüft. Bei Erfüllung der Zeitbedingungen zur Bearbeitung einer solchen Routine wird direkt in diese verzweigt.

Behält man den Umstand im Auge, daß dieses Unterprogramm bei entsprechender Compiler-Steuerung nach jeder BASIC-Zeile erneut ausgeführt wird, erwartet man eine deutlich spürbare Verlangsamung. Die Compiler-Bauer bemühten sich daher hier ganz besonders um extrem schnellen Maschinencode. Trotzdem lohnt es sich, bei laufzeitintensiven Schleifen die $U-Aufrufe per Hand auszudünnen.

Ich möchte herausheben, daß die kontinuierliche Abarbeitung einer Interrupt-Routine mittels EVERY ... unbedingt des Setzens der $U-Aufrufe in zeitlich nicht allzu weit entfernten, regelmäßigen Abständen bedarf.

Tatütata!

Erkennt eine Bibliotheksfunktion einen Laufzeitfehler wie z.B. ‘Logarithmus von 0’, so darf sich eine weitere Bibliotheksfunktion bewähren, nämlich die interne Fehlerroutine ‘ERROR’.

Dieser Maschinenroutine fällt es zu, die durcheinandergeratenen Register wieder auf die Reihe zu kriegen. Abschließend verzweigt ‘ERROR’ zur Fehlerbehandlungsroutine, mit der Anweisung ‘ON ERROR GOSUB errproc’ hoffentlich voreingestellt (falls nicht: Programmabbruch). Am Ende der Fehlerbehandlungsroutine sollte die Anweisung RESUME... stehen. Während der Bearbeitung dieser Anweisung geschieht folgendes:

Der Stack wird - egal ob RESUME, RESUME NEXT oder RESUME Label - vollständig zurückgesetzt, ohne daß die auf ihm gespeicherten Daten besonders ausgewertet würden. Mit dem Heap wird wesentlich dezenter verfahren, er wird korrekt bis zur richtigen Höhe abgearbeitet.

Der bei der letzten $U-Routine gespeicherte pc erhält durch den Rücksprung mittels RESUME bzw. RESUME NEXT eine besondere Bedeutung. Bei RESUME wird dieser pc als Wiedereinstiegsadresse herangezogen, bei RESUME NEXT wird anhand der oben erwähnten Tabelle die nächste $U-Routine gesucht. Das geschieht so: Auf die Startadresse des Programms werden solange die einzelnen Einträge dieser Tabelle aufaddiert, bis diese akkumulierte Adresse größer ist als der gespeicherte pc. Diese Adresse wird als neuer pc eingesetzt.

Das Kochrezept

Die obigen Ausführungen ziehen einige Konsequenzen nach sich, die unbedingt beachtet werden müssen:

Zum einen, angenommen, der $U-Aufruf vor einer fehlererzeugenden BASIC-Zeile würde die EVERY-Prozedur aktivieren. Die $U-Aufrufe innerhalb der EVERY-Prozedur würden dann den pc wieder und wieder überschreiben. Nach Rückkehr aus der EVERY-Prozedur bis zur fehlererzeugenden Zeile erfolgt jedoch kein weiterer $U-Aufruf. Ein RESUME NEXT hat die Programmfortführung nach der EVERY-Prozedur zur Folge.

Zum anderen würde in der Interrupt-Routine eine erneute Prüfung der EVERY-Bedingung vorgenommen. Da keine Semaphor-Variable den erneuten Aufruf der EVERY-Prozedur verhindert, kann es bei entsprechend langen Bearbeitungszeiten zu rekursiven Aufrufen der Interrupt-Routine kommen. Im Extremfall beschäftigt sich das Programm nur noch mit dieser Prozedur (Demoprogramm 3).

Ich hoffe, der GFA-Gemeinde mit den obigen Ausführungen einige leidvolle Erfahrungen zu ersparen.

' Testprogramm 1
'
' Demonstration von $U+ innerhalb der 
' Fehlerabfangroutine.
'
$U+,$P>
'
ON ERROR GOSUB errproc
'
PRINT "Programmstart"
REPEAT
UNTIL INKEY$<>"" 
'
a=200/0
'
PRINT "Prommende"
REPEAT
UNTIL INKEY$<>""
END
'
$U-
PROCEDURE errproc
    ALERT 1, "Fehler "+STR$(ERR)+"|aufgetreten",2,"resume|res next",e&
    ON ERROR GOSUB errproc 
    IF e&=1 
        $U
        PRINT "Endlosschleife einmal anders <Taste>"
        '
        IF INKEY$<>"" 
            END 
        ENDIF
        '
        RESUME
    ELSE
        $U
        RESUME NEXT
        $U
        PRINT "falsches RESUME NEXT"
        ERROR 7 
    ENDIF 
RETURN

' Testprogramm 2 ' ' Demonstration von $F< mit nachfolgendem ' Fehler. Die Fehlerroutine scheint einwandfrei ' zu arbeiten, erst später erscheinen unerwartet ' Bomben. ' $U+ ON ERROR GOSUB errproc test_p_kleiner PRINT "Dieser Text wird nie ausgegeben" END ' $P< PROCEDURE test_p_kleiner a_test ' PRINT "Returnadresse fehlt, gleich gibts Bomben <Taste>" REPEAT UNTIL INKEY$<>"" RETURN ' $P> PROCEDURE a_test x%=0 PRINT "fehlererzeugender Aufruf <Taste>" REPEAT UNTIL INKEY$<>"" b_test ' x%=10 PRINT "fehlerfreier Aufruf <Taste>" REPEAT UNTIL INKEY$<>"" b_test RETURN ' $P> PROCEDURE b_test a=c%/x% RETURN ' $U-,$P> PROCEDURE errproc x%=x%+1 ALERT 1,"Fehlerroutine aktiv",1,"res next",e& RESUME NEXT RETURN
' Testprogramm 3
'
' Demonstration von $U+ in Interruptroutinen
' ------------------------------------------
' Erzeugt kleines Chaos auf dem Bildschirm 
' und läßt Fehlerbehandlung ins Abseits laufen.
' Mit den Tasten <i> und <d> kann die Bearbeitungs-
' zeit der Interruptprozedur geändert werden,
' beim Selbstaufruf von rupt erfolgt automatisch 
' eine Verkürzung.
'
$U+,$I+
$P>
'
ON ERROR GOSUB errproc 
EVERY 5 GOSUB rupt
'
l%=2
schleife
END
'
PROCEDURE schleife 
    DO
        t$=UPPER$(INKEY$)
        SELECT t$
        CASE ""
            ' nichts weltbewegendes 
        CASE "E"
            PAUSE 10 !damit rupt bearbeitet wird
            ERROR 1 
        CASE "X"
            EXIT IF TRUE 
        CASE "I"
            l%=SUCC(l%)
            PRINT
            PRINT "I ";l%
        CASE "D"
            l%=MAX(PRED(l%),1)
            PRINT
            PRINT "D ";l%
        DEFAULT
            EVERY STOP 
            PRINT
            PRINT "E: Fehler erzeugen"
            PRINT "I: EVERY PROC verlängern"
            PRINT "D: EVERY PROC verkürzen"
            PRINT "X: Programmende"
            EVERY CONT 
        ENDSELECT 
    LOOP 
RETURN
'
$U+
PROCEDURE rupt
    DEC l%      ! Bearbeitungszeit kürzer
    PAUSE 1%
    ' hier wird EVERY-Bedingung geprüft,
    ' und lange Pausen ergeben: erfüllt !!!
    PRINT " A";
    ' aber auch hier ...
    PRINT l%;
    INC l%      ! wieder normal
RETURN 
PRINT
PRINT "Guten Morgen"
END
'
$U-
PROCEDURE errproc
    ALERT 1,"Fehler "+STR$(ERR)+"|aufgetreten",1,"res next",e&
    ON ERROR GOSUB errproc 
    RESUME NEXT 
RETURN

Wolfgang Ettig
Links

Copyright-Bestimmungen: siehe Über diese Seite