zurück : weiter : inhalt =

4.5 *** Basic

Die Basic-Programme selbst sind nicht Thema. Dazu gibt es das Handbuch, den "User Guide", und noch viele andere Schriften. Wir betrachten ausschließlich die vorgesehenen Methoden zur Erweiterung des Basic-Befehlssatzes.
 

4.5.1 ** Residente Erweiterungen

Das SuperBASIC des QL dürfte kaum von Konkurrenz bedroht sein. Doch, wie in allen interpretierenden Programm(ier)-sprachen, gibt es hier soviele Einzelheiten zu verwalten, daß der direkt programmierte Processor-Code wesentlich schneller zum Ziel kommt. Es ist gelegentlich ein Faktor um 1000 erreichbar. In vielen Fällen ist der Aufwand für das Schreiben besonderer Basic-Befehle nicht allzu hoch, der Gewinn dagegen ganz außerordentlich.
Auch ist manche Einrichtung des QL sonst unzugänglich.

Schließlich sind Editoren, direkte Befehlseingabe und narrensichere Fehlerkontrolle sehr angenehm bei der Programmentwicklung, jedoch in einem ausgetesteten Programm als Ballast entbehrlich. Was zum Compilieren führt, womit allein sie schon entschieden schneller werden.

Daß Basic-Programme compilierbar sind, wäre ansich kaum der Rede wert, ist das doch allgemein üblich. Im Ql werden sie aber dadurch zugleich selbständige Programme. Sie sind tauglich zum Multitasking und den anderen Jobs völlig gleichgestellt. Bedenkt man noch, daß diese vorher als Basic-Programme Stück für Stück aufgebaut und in Teilen bereits vollständig geprüft werden können, so wird die Erweiterbarkeit des Basic zu einer besonders wichtigen und hilfreichen Einrichtung des QL.

Genaues Einhalten der im Basic angewandten Verfahren ist Voraussetzung für die Compilierbarkeit von Programmen, in denen auch Basic-Erweiterungen enthalten sind. Natürlich gilt das für sämtliche Anwendungsfälle. Dabei ist das System aber so außerordentlich fehlertolerant, daß so mancher Erweiterungscode unter einfachen Umständen anwendbar ist, ohne daß irgendeine Fehlfunktion aufträte. Darum der Rat, sich strikt an die dokumentierten Konventionen zu halten, und notfalls die Programme unter möglichst irrwitzigen Bedingungen zu prüfen, damit nicht doch einmal eine Fehlermeldung das Ganze anhält.
 

4.5.2 ** Anmeldung

Irgendein (nahezu) beliebiger Code kann dem Interpreter als Befehl verfügbar gemacht werden. Das ist bereits bekannt. Jetzt soll das Verfahren dazu beschrieben werden, den Angaben zu den eigentlichen Bedingungen vorgreifend, unter denen solche Erweiterungen zu arbeiten haben.

Das Hilfsmittel hierzu ist der Vector

BP.INIT $0110
Ihm ist eine Tabelle mitzugeben, in der alle neuen Befehle verzeichnet sind. A1 übergibt die Adresse dieser Tabelle. Bei der Ausführung werden D2 und A1 verdorben, die anderen Register bleiben erhalten.
Der Stack wird nie mit mehr als 48 Bytes beansprucht.

Die Tabelle enthält:
 

proc_num DC.W {anzahl.proceduren}
  DC.W erste.proc - pc 
  DC.B erste.len,"{erster.name}"
  DS.W 0
  ...  
  DC.W letzte.proc - pc 
  DC.B letzte.len,"{letzter.name}" 
  DS.W
func_num DC.W {anzahl.funktionen}
  ...   
  DC.W 0
 

Alle Eintragungen müssen auf geradzahliger Adresse beginnen, darum sind die DS.W-Angaben gemacht. Assembler justieren dann gewöhnlich entsprechend. Bei anderen als dem GST-Assembler können andere Mnemonics erforderlich sein. Natürlich kann die Angleichung auch durch Anfügen irgendeines Füllzeichens an den Namen geschehen. Der Autor hat sich obige Schreibweise zerstreutheitshalber für die Sicherheit angewöhnt. Eine Macro-Definition dazu, der nur noch die Namen anzugeben sind, ist {13} beigefügt.

Die erste Eintragung der beiden Tabellenteile gibt die Anzahl der folgenden Namen an. Sie sollte genau deren Zahl entsprechen. Nur, wenn die durchschnittliche Länge der Namen Sieben überschreitet, ist jeweils eine größere Zahl einzutragen:

Zahl = (Summe aller Schriftzeichen + Anzahl aller Funktionen + 7) DIV 8
Sind von einer Sorte keine Definitionen anzugeben, trägt man als Anzahl Null ein, danach gleich wieder Null als Marke für das Ende des Bereiches. Die vor den Namen angeordneten Zahlen sind Pointer auf die zugehörigen Aufrufe relativ zur eigenen Adresse.

Eine übliche Notation ist etwa auch

DC.W startlabel-*

4.5.3 ** Struktur des Basic

SuperBASIC, genauer der Basic-Interpreter, ist ein Job.
Er findet sich solchermaßen geordnet an nicht vorhersagbarer und jederzeit, selbst während des Programmablaufs, veränderlicher Position im Speicher:
 
  1. Job Header Kopf mit einigen Job-Daten 
bp   2. Work Area Arbeitsbereich des SuperBASIC
  3. Name Table Tabelle mit Daten aller Namen
  4. Name List Die Namen selbst
  5. Variable Values Bereich der Variablenwerte
  6. Channel Table Kanaltabelle
  7. Arithmetic Stack (dieses) 
  8. Token List Token
  9. Line Number Table Zeilennummern
10. Program File tokenisiertes Programm 
11. Return List (eine Art Buchführung mit Stack) 
12. Buffer Datenpuffer 
 
Der Interpreter enthält daneben noch ein Anzahl Stacks und Tabellen, wie die im Tabellenteil verzeichneten Basic-Variablen andeuten. Die Tiefen des Basic-Interpreters sollen hier nicht weiter interessieren, das ginge weit über das Thema hinaus. Insofern sind diese Angaben belanglos, ebenso o.a. Posten 1 und 8 bis 11.

Wenn nun das Wort Basic fällt, ist damit seine innere Gestalt als Programm im Processorcode gemeint. Auf die Relation zur Jobbasis A6 deutet die gelegentlich angefügte Abkürzung (bp) - basic pointer - hin. Grundsätzlich sollen hier aber alle Adressen stets relativ zu diesem Ptr gelten. Auf absolute Adressierung wird immer besonders hingewiesen.

Insbesondere verbietet es sich, "nur kurz einmal" A6 etwa auf Null zu setzen, um mit einer relativ bewerteten Adresse absolut arbeiten zu können {8}. Denn ein Basicjob kann sich jederzeit zwischen beliebigen zwei Befehlen verlagern. Egal wie oft, irgendwann geschieht das, es kündigt sich nicht an, und passiert auch nicht nur so ein bischen, so, daß es sich nicht auswirken würde: Merkmal des "Digitalen" ist ja gerade, daß es einen wie auch immer gearteten Intensitätsunterschied nicht gibt. Das Phänomen nimmt impertinenterweise auch keinerlei Rücksicht auf die sonstige Reputation des jeweiligen Autors.
Für den Programmablauf ist es völlig bedeutungslos, wie häufig das geschehen kann oder mit welcher Wahrscheinlichkeit.

Das System bietet hinreichend sichere Möglichkeiten der Adressierung:
Da sind relativ adressierbare und "mitwandernde" Puffer vorhanden, es lassen sich Puffer auf fester Adresse einrichten, und es gibt(!) die Subtraktion.
Man kann auch dafür sorgen, daß alle Interrupts abgeschaltet sind und das Programm selber eine Bereichserweiterung mit Sicherheit nicht auslöst.
Auch die Qdos-Trap #4 wird des öfteren von Nutzen sein.
 

Arbeitsbereich BV.START 00(bp)

Der Arbeitsbereich wird mit A6 und Pointern adressiert, die den Abstand dazu angeben. Sein Aufbau ist den Tabellen der Basic-Variablen zu entnehmen.

Die Basisadresse A6 wird nur vom Sytem angerührt, wenn nämlich der Job an eine andere Stelle zu schieben ist. Durch Programmierung äußerstenfalls mittels der beiden Traps für Speicherzuweisung und -Freigabe, MT.ALBAS (1/22) und MT.REBAS (1/23). Diese verlagern damit zugleich auch als Einzige den Stackpointer.

Name Table BV.NBAS 24(bp) bis vor BV.NT 28(bp)

Alle seine Referenzen in procedural aufgebaute Programmteile sowie auf Variable bezieht der Interpreter auf die Daten der Name Table.

Für jeden irgendwann einmal auch nur genannten Namen enthält diese eine Eintragung von 8 Bytes Länge. Zusätzlich werden in Reihenfolge der Übergabe bei jeder Parameterdeklaration für Funktionen und Proceduren Copien der betreffenden Daten am Ende der Tabelle angelegt, mit denen dann die eigentliche Arbeit erfolgt. Nach Beendigung des Aufrufs ordnet das System die möglicherweise auch veränderten Daten wieder in die Tabelle ein.

SQ: Uneinheitlich(! s.u.) und abweichend von der Standarddefinition werden dort Variable bereits durch Nennung des Namen initiiert. Fehler aufgrund "unbekannter" Variabler erscheinen dann nicht mehr und können dadurch dubiose und unergründliche Folge-Fehler auslösen. Deren Ursprung tritt, wenn überhapt, bestenfalls durch Scheitern des betr. Programms zutage, oft aber auch nur dadurch, das solch ein Programm zwar spielt, seine Aufgaben aber nicht so recht erfüllen will.
Gezielt sind diese Fehler kaum mehr aufspürbar.
Einzig außerhalb von Proceduren/Functionen stehende Variable haben einen - undokumentierten, darum keineswegs sicheren - Status, der mit einiger Mühe als "undefiniert" erkennbar ist.

Locale Variable sind dadurch realisiert, daß die LOCal deklarierten Namen wie die der Parameterliste zwar copiert, zum Schluß aber verworfen, d.h. nicht in die ursprünglichen Posten zurückgespeichert werden.

Die 8 Bytes einer Eintragung bedeuten:

Name List BV.NLBAS 32(bp) bis vor BV.NLP 36(bp)

Hier stehen sämtliche Namen verzeichnet. Die Längenangabe ist in nur einem Byte vor dem Namen gespeichert, sodaß die Grenzen zwischen den Eintragungen auch auf ungerader Adresse stehen können.

Bytes 2 und 3 der Name Table enthalten den Abstand der Basisadresse aus BV.NLBAS zum Längenbyte des zugehörigen Namen. Umgekehrt läßt sich aus einem Namen die zugehörige Eintragung nur durch Absuchen der gesamten Name Table finden. An wievielter Stelle der Name steht, ist gleich dem Index in die Name Table. Dieser mit 8 multipliziert gibt den Abstand zur richtigen Eintragung an.

Damit ist die Gesamtlänge aller je genannten Namen eine Basic-Jobs einschließlich ihrer Längenbytes auf 32K Bytes beschränkt. Wenngleich die rechnerbedingte Grenze erst bei 64K liegt, darf diese Zahl nicht überschritten werden, da die Bewertung des betr. 16-Bit-Wortes je nach QDOS-Variante vorzeichenbehaftet erfolgt:
Aus unerfindlichem Grunde scheinen "arithmetisches" Schieben (insbes. ASR) und vorzeichenbehaftete Flagbewertung (z.B. BLT statt BCC, etc) als betr. Grundoperation angesehen zu werden. Zahlen sind aber gänzlich unschuldige Wesen, die selber keinerlei Bewertung vornehmen. Die unglücklich gewählte Vorgabe für Datenposten in 16-Bit Wortgröße bei gängigen Assemblern tut das Ihre dazu.
Beides ist Unsinn und führt sehr leicht zu Fehlern durch Vorzeichenüberlauf - Belege gibt es in großer Zahl und an durchaus prominenter Stelle (QDOS, PIF bis 1.68(?).

Variablenwerte BV.VVBAS 40(bp) bis vor BV.VVP 44(bp)

Diese stehen in einem bedarfsweise ggf. auch nur für die Übergabe zugewiesenen Speicher-Block. Sie sind ohne weitere Angaben zu holen mit einem zugehörigen Vector und den beim Einsprung aus dem Basic mitkommenden Werten in A3 & A5 als Ptr in die erweiterte Name Table. Neben A1 als Ptr auf den Anfang des ersten Parameters kommt in D3 deren Anzahl zurück.

Die Übergabe eines Wertes an Basic-Programme geschieht mit der Rückkehr (RTS) aus einer Funktion, muß aber einem Protokoll folgen, das den Fehlercode, die Typenmarkierung und die Art der Speicherung bestimmt. Weitere Parameter oder Variable können mittels BP.LET auf neue Werte eingestellt werden.

Anordnung der Werte im Speicher und die Möglichkeiten des Zugriffs darauf werden genauer im Anschluß an diese Zusammenstellung beschrieben.
 

Kanaltabelle BV.CHBAS 48(bp) bis vor BV.CHP 52(bp)

Enthält die einem Basic-Job zugeordneten Kanäle samt einigen Zusatzangaben:
 

CH.IO 0   Kanalidentifikation (ID des Qdos-Kanals) 
-1 bei nicht mehr existierendem Kanal
CH.CCPY 4 f.p. Horizontale Graphik-Cursorposition (y)
CH.CCPX 10 f.p. Vertikale Graphik-Cursorposition (x)
CH.ANGLE 16 f.p. Winkel der Turtle-Graphik
CH.PEN 22 .b PEN-Status, =/= 0 nach PENDOWN
  ...    
CH.PPOS 32 .w laufend bis WIDTH-Grenze gezählte Zeichenposition
CH.WIDTH 34 .w mit WIDTH gesetzte Breite einer Ausgabezeile
Die Tabelle ist - "derzeit" nach {1,2} - 40 Bytes lang, in den dem Verfasser bekannten Systemen JM bis SMSQ 2.76 wurde das nicht geändert.

Ausgehend von der Basis in BV.CHBAS wird der Tabellenplatz eines Kanals folgendermaßen ermittelt:
 

ofs = Kanalnummer * 40 ist der Offset in die Tabelle 
bse = BV.CHBAS(bp) Tabellenbasis aus der Variablen
tab = (ofs + bse)(bp) Ptr auf die Kanaldaten 
 
Hat der Ptr den Wert aus BV.CHP(bp) erreicht, gibt es einen Kanal der angegebenen Nummer nicht. Ist das erste Langwort der Tabelle negativ, wurde er zuvor geschlossen.

Also z.B. mit der Basic-Kanalnummer in D0:

bchan mulu  #40,d0 kabellenposition
add.l  48(a6),d0 bv.chbas kanaltabelle
cmp.l 52(a6),d0 Obergrenze
bcc.s kanal_nf  ? erreicht
move.l 0(a6,d0.l),d0  sollte die ID sein
not.l d0 auf flag -1 prüfen
bne.s ist_okay ? ist kanal-ID
moveq #-6,d0 sonst ERR.NO
rts 
kanal_nf  moveq #6,d0 kein Kanal, ERR.NF
ist_okay  not.l d0
rts
 
 

Arithmetik-Stack BV.RIP $58(bp)

Hier ist die Stelle verankert, die den Rechneraufrufen als Basisadresse für ihren Stack dient. Zugleich werden von hier aus Werte aus residenten Funktionen an Basic übergeben. Aber auch für sonstige Speicheroperationen kann dieser Bereich als Kurzzeitspeicher benutzt werden.

Dabei ist zu beachten, daß er, ggf. mit Hilfe der Trap #4, nur A6-relativ adressiert werden darf, und daß sich seine Adresse durch jeden Aufruf eines QDOS-Vectors, der irgendwelche Aktionen im SBasic erledigt, verändern kann, Sichern seiner Adresse in solchen Fällen also zwecklos ist. Gleiches gilt für den Fall, daß inzwischen ein neuer Job eingerichtet wurde - womit jederzeit gerechnet werden muß, da das auch durch beliebige andere Programme geschehen kann.
Alle Basic-Pointer müssen nach solchen Vorgängen bedarfsweise immer wieder neu aus ihrer Variablenposition gelesen werden.

Platzreservierung geschieht hier mittels BV.CHRIX, wie anschließend und bei der Variablenbeschreibung näher erläutert. Der Ptr in BV.RIP ist dabei immer durch das Programm zu aktualisieren, wenn die Reservierung fixiert werden soll.
 

Pufferbereich BV.BFBAS 00(bp)

Nur soviel, daß in BV.BFBAS auf 00(bp) die Adresse eines kurzfristig frei nutzbaren Pufferbereiches steht. BV.BFP auf 04(bp) enthält einen Laufptr dorthinein.  Die meisten SBasic- und TK2-Aufrufe, die irgendwie mit Zeichenketten umgehen, verwenden diesen Puffer. WCOPY z.B. ist genau darum so bemerkenswert langsam.
 
 

4.5.4 ** Parameter

4.5.4.1 * Allgemein

Parameterübergabe:

Zum Holen von Parameterwerten, deren Gestalt im Folgenden beschrieben werden wird, gibt es eigene Vectoren für vier Parametertypen. Ihre Verwendung ist dort angegeben. Weiter einen, mit dem Variablen ein neuer Wert zugewiesen werden kann. Variablenfelder entziehen sich diesem Zugriff. Deren Aufbau ist am Schluß genauer dargestellt.

Die Trennzeichen zwischen den übergebenen Parametern, Separatoren, sind im zweiten Byte der übergebenen Parametertabelle - auf 1(a3,a6.l) - codiert:

'#'-Marke "Hash" durch gesetztes Bit 7 angezeigt.

Separator in Bits 4 bis 6 mit

%.000.... 0 ohne Trennzeichen
%.001.... 1 Komma
%.010.... 2 Semicolon
%.011.... 3 Bachslash ("\")
%.100.... 4 Ausrufezeichen
%.101.... 5 "TO"
Typ in Bits 0 bis 3 wie oben beschrieben.

Somit kann, auch zur Laufzeitersparnis, schon vor dem Holen die Art eines Parameters leicht ermittelt werden. Die Anzahl Parameter ist die durch acht geteilte Differenz A5 - A3, steht also auch bereits vorher zur Verfügung. Hiervon wird etwa im Beispiel "DEFVAL" Gebrauch gemacht.

Leider spielt auch hier das SMSQ wieder einmal nicht so recht mit. Ein Geistesblitz der besonderen Art muß wohl zu der Wiedereinführung der schon in uralten Basicvarianten lästigen automatischen Initiierung von Variablen auf einen festen Inhalt geführt haben (Null rsp. Leerstring). "Undefinierte" Variable im Sinne des SuperBASIC gibt es dort nur außerhalb von FuNctionen oder PROCeduren, also gerade dort nicht, wo entsprechende Initiierungsaktionen in programmierter Form ihren größten Nutzen entfalten würden.

Eine besondere Einrichtung des QL bewirkt, daß die übergebenen Parameter wenn irgend möglich stets in die angeforderte Form gebracht werden. So kann eine Zahl auch als String geholt werden, jede 32-Bit-Zahl als f.p.-Zahl (in welcher Form sie ohnehin gespeichert ist) und jede Integer-Zahl in einer der anderen Formen.

Umgekehrt müssen die Variablen "aufwärtscompatibel" sein:
Eine f.p.-Zahl, die als Integer geholt werden soll, muß in passendem Wertebereich angegeben sein, ein String muß mit einer Ziffer beginnen, einem "e" muß stets eine Ziffer folgen.

Der erste Parameterwert steht an der Adresse 0(A1,a6.L).

Falls sich ein Fehler ergibt, der auf falscher Typenwahl beruht, kann ohne weiteres ein anderer Vector aufgerufen werden.

Wenn nicht das Programm selbst A3 & A5 verändert hat, ist der Parameterwert nur zerstörungsfrei gelesen worden. Trennzeichen und "hash"-Marke werden beim ersten Vectordurchgang überschrieben, müssen also ggf. vorher gesichert werden (s.u.). Auch wird der Rechnerstack nicht immer richtig wieder freigegeben, wogegen nur vorheriges Sichern der Stacktiefe hilft, und im Fehlerfalle Rückstellen auf diesen Wert. Da allein dies stets zuverlässig ist, im Gegensatz zur Freigabe nach Abzählen, wird es weiter unten genauer beschrieben.
Zur Erkärung:
In allen ROMs gibt es Fehler in der Basic-Speicherbelegung nach fehlgeschlagener Umwandlung von Strings in Zahlen. Der durch die Umwandlungsroutinen wirklich reservierte und u.U. nicht wieder freigegebene Bereich kann weit größer sein, aber auch kleiner. Freigabe eines rechnerisch auf Grundlage der angenommenen Anzahl Posten und deren Größe ermittelten Bereichs kann dann den Basicspeicher verderben.

Typvorgabe für Variable:

Funktionen geben einen Wert zurück. Deren Typ ist in Basicprogrammen durch die Endung des Namen gegeben, residente Funktionen haben dagegen keinen vorgegebenen Typen. Die Endung hat hier nur kennzeichnenden Wert. Eine Funktion MÜLL% kann also durchaus den String "EIMER" zurückgeben (Umlaute im Namen übrigens erst ab MGx).

Die Rückgabetypen residenter Funktionen sind in D4 wie folgt anzugeben:
 

1 String (Zeichenkette)
2 Fließkommazahl
3 Ganzzahl (16-Bit-Zahl)
 
Speicherzuweisung:

Ein zurückzugebender Wert wird zunächst in die gewünschte Form gebracht. Die Basicvariable BV.RIP(bp) bekommt die Adresse, auf der der Wert steht. Dies darf jedoch keine beliebige (relative) Adresse irgendeines Puffers sein. Vielmehr ist das der Basispointer auf den Rechnerstack, der z.B. mit dem Vector RI.EXEC benutzt wird. Es ist zugleich der allgemeine Parameterbereich und Hilfsspeicher für alle arithmetischen Operationen.

Platz wird dort auf verschiedene Weise reserviert. Die genannten Vectoren bewirken das für alle von ihnen geholten Parameter bereits genau passend.

Der gesamte Bereich kann vollständig aufgegeben werden, nachdem(!) alle Daten entnommen wurden. Nach fehlerfrei verlaufenem Vectordurchgang kann die Anzahl der freizugebenden Bytes mit Hilfe der wirklichen Anzahl und des Wissens um die Länge der angekommenen Parameter ermittelt werden. Die Basic-Variable BV.RIP wird, ggf. geradzahlig justiert(!), um den entsprechenden Betrag erhöht, womit der Bereich bereits wieder allgemein verfügbar ist.

Bei Strings wird das gelegentlich allzu umständlich werden, deren Bereiche gibt man am einfachsten einzeln für jeden String frei, oder en bloc mit der nach u.g. Methode ermittelten Bereichsgröße.

Eine von allen Situationen unabhängige und insbes. auch im Fehlerfalle stets zuverlässige Form der Freigabe besteht im Rückstellen auf die vor dem Aufruf ermittelte Stacktiefe:
 

parm_holen move.l $58(a6),a4 bv.rip holen und mit
sub.l $5c(a6),a4 bv.ribas stacktiefe ermitteln
.... (hier die parameter holen)
add.l $5c(a6),a4 stacktiefe und bv.ribas
move.l a4,$58(a6) wird bv.rip für leeren stack
...
tst.l d0 move in den speicher hat die flags verändert!
 
Der Umweg über die Stacktiefe ist notwendig, da die Zahlenwerte von BV.RIP und BV.RIBAS sich auch bei gleichbleibendem Stack ändern können. Ausgelöst werden solche Änderungen u.a. beim Holen sehr langer Strings, wenn deren Länge dazu führt, daß der Basic-Job selbst neuen Speicher aufnimmt, oder dadurch, daß während der Parameterverarbeitung irgendein anderes Programm einen neuen Job einrichtet, wodurch alle Basic-Interpreter-Jobs im Speicher verlagert werden.

Für zusätzlich benötigten Speicher und zum Platzmachen für die Rückgabe eines Funktionswertes gibt es den Vector BV.CHRIX, $011A dessen Einsatz weiter unten ausführlich beschrieben ist.

Damit Funktionen Ergebnisse liefern, mit denen gleich auch gerechnet werden kann, und die bei Verlagerung des Basic immernoch sicher arbeiten, ist BV.RIP unbedingt und peinlich genau auf aktuellem Stand zu halten. Bei der Rückkehr aus residenten Proceduren wird der Parameter-Speicher vom Interpreter selbst bereinigt, Funktionen müssen dagegen mit genau der richtigen Bereichsgröße und -Lage(!) abgeschlossen werden.
Und stets ist dafür zu sorgen, daß alle Operationen nur innerhalb des fixierten Bereiches stattfinden.
 

4.5.4.2 * Integer-Zahlen

CA.GTINT  $0112
Sie stehen als 16-Bit-Posten an der angegebenen Adresse, im Basic als vorzeichenbehaftete Zahlen mit Darstellung negativer Werte im Zweiercomplement verarbeitet. Trotz der eigentlich nur benötigten 2 Bytes liegen diese Zahlen in einem Feld von 6 Bytes Größe im Variablenbereich.

Der Aufruf <getint> holt eine 16-Bit-Ganzzahl in Wortlänge nach D5:

v_vec equ $0112 Vector CA.GTINT 
v_len equ Länge einer Zahl 
getint move.l $58(a6),a4 bv.rip
sub.l $5c(a6),a4 bv.ribas
move.w v_vec,a2 QDOS-vector
jsr (a2) > aufrufen
bne.s errxit ? fehler
moveq #-15,d0 err.bp vorbereiten
subq.w #1,d3 ein parameter
bmi.s errxit ? reicht nicht
moveq #0,d0 flag: fehlerfrei
fetchp move.w 0(a1,a6.l),d5  wert holen, überzählige sind egal
errxit add.l $5c(a6),a4 mit bv.ribas
move.l a4,$58(a6) bv.rip zurückstellen
tst.l d0 flags setzen
rts
 
Zur Rückgabe aus einer Funktion sind anzugeben
D4 = 3 für den Variablentyp Integer
D0 = 0 meldet fehlerfreien Verlauf
Es sei die Zahl in D5.w gegeben. Dann folgt etwa:
retint moveq #2,d1 zwei bytes platz
move.w $11a,a1 mit bv.chrix
jsr (a1) > sicherstellen
move.l $58(a6),a1 bv.rip
subq.l #2,a1 justieren
move.l a1,$58(a6) platz fixieren
move.w d5,0(a1,a6.l)  wert ablegen
moveq #3,d4 typ integer markieren
moveq #0,d0 und fehlercode
rts
 

Wobei auch hier die frei definierten Fehlermeldungen angegeben werden können, wie das bei der Beschreibung der Access Layer einfacher Device Handler (3.3.2.3.2) angegeben wurde.
 

4.5.4.3 * Flieszkommazahlen (f.p.)

CA.GTFP $0114
Diese liegen mit einem 12-Bit-Exponenten in zwei Bytes und weiteren vier Bytes mit 32-Bit-Mantisse vor. Die Mantisse ist vorzeichenbehaftet im Zweiercomplement notiert. Die Notation des Exponenten erfolgt mit einem "Offset" von $0800, wo dann Bit 11 eines negativen Exponenten Null ist.

Die Zahl wird so normiert, daß die Mantisse im Bereich 1/2 bis -1/2 liegt. Bit 31 zählt darin mit -1. Weitere Konventionen gibt es nicht.

Die Bits haben die Wertigkeit 2^(Bitnummer-31), also etwa

Bit 30: 2^-1 = 1/2 = 0,5
Bit 21: 2^-10 = 1/1024 = 0,00097656
Dabei gibt es die Ausnahmen
Bit 31: -(2^0) = -1
Alle Bits 0 = Mantisse der Zahl Null
Die ganze Zahl
Zahl = Mantisse * 2 ^ (exponent - $81F)
Als Aufruf zum Holen einer f.p.-Zahl läßt sich das Muster <getint> verwenden, wenn man den Lesebefehl anpaßt und definiert
v_vec equ $0114 Vector CA.GTFP
v_len equ 6 Anzahl Bytes einer Zahl
...
move.w 0(a1,a6.l),d5 (erster) exponent
move.l 2(a1,a6.l),d7 (erste) mantisse
...
 
Zur Rückgabe aus einer Funktion sind anzugeben
D4 = 2 für den Variablentyp f.p.
D0 = 0 meldet fehlerfreien Verlauf
Nach Anforderung und Reservierung von 6 Bytes Platz im RI-Stack wird die Zahl analog o.a. Beispiel <retint> aus der SBasic-Funktion zurückgegeben.
 
 

4.5.4.4 * 32-Bit-Zahlen

CA.GTLIN $0118
Für Zahlen im Wertebereich -2^31 bis 2^31-2 gibt es diesen Vector, mit dem 32-Bit-Zahlen, f.p. oder Integer passend gemacht, geholt werden.

Am oberen Ende des Wertebereichs wäre 2^31-1 ansich die richtige Grenze. Akzeptiert wird dafür im Basic aber nur die Eingabeform 2^30-1+2^30.

Zum Holen einer Zahl gilt das Beispiel zu ca.gtint entsprechend mit

v_vec equ $0118 Vector CA.GTLIN
v_len equ 4 Anzahl Bytes einer Zahl
...
move.l 0(a1,a6.l),d5 lange ganzzahl (integer)
...
 
Rückgabe solcher Daten ist direkt nicht möglich, da ein entsprechender Parametertyp nicht existiert. Die Zahl kann entweder, wenn der Wertebereich im erlaubten Rahmen bleibt (- 32768 bis 32767) als Integer abgeliefert werden, oder normiert als f.p.-Zahl.

Eine einfache Routine zur Normierung und Rückgabe der Zahl aus D5:

retfp moveq #6,d1 RI-speicher zur rückgabe
move.w $11a,a1 bv.chrix
jsr (a1)
move.l $58(a6),a1 bv.rip
subq.l #6,a1
move.l a1,$58(a6)
move.l d5,0(a1,a6.l) 
beq.s retfnul ? fertig bei null
hier move.w #$081f,d0 höchster exponent
normfp add.l d5,d5
dbvs d0,normfp vorzeichenbits hochschieben
roxr.l #1,d5 normiert mit vorzeichen
move.w d0,0(a1,a6.l)  exponenten eintragen
retfnul move.l d5,2(a1,a6.l)  mantisse
moveq #2,d4 typ f.p.
moveq #0,d0 ohne fehler
rts
 
An der Stelle <hier> könnte beispielsweise noch eine grob optimierende Sequenz eingesetzt werden:
hier move.w d5,a0 Vorzeichenerweiterung implizit
cmp.l a0,d5 Vergleich mit Vorzeichen
beq.s retfint ? integer bei 15-Bit-Betrag
move.w #081f,d0
normfp ...
... 
retfint addq.l #4,$58(a6)
move.w d5,4(a1,a6.l)
moveq #3,d4 typ integer
moveq #0,d0 ohne fehler
rts
 
Weitergehende Optimierung lohnt im allgemeinen den Aufwand nicht; bei MINERVA bietet sich stattdessen das entsprechende RI.EXEC-Kommando an (ri.flong, D0:=9).
 
 

4.5.4.5 * Strings

CA.GTSTR $0116
Strings sind immer auf geradzahliger Adresse beginnend mit dem Längenwort gespeichert. Bei ungerader Länge steht am Ende ein Füllbyte.

Nach dem Holen eines String kann im Gegensatz zu den anderen Aufrufen der Übergabestack nur freigegeben werden, wenn der String in einen anderen Bereich übernommen wurde. Sonst gehen die Daten verloren. Man übernehmimmt nun zwar die Länge in D5 und passt den Pointer A1 an, läßt den Speicher aber zweckmäßig zunächst unberührt.

Einen String holen, Länge nach D5:

v_vec equ $0116 Vector CA.GTSTR
getstr move.l $58(a6),a4 bv.rip
sub.l $5c(a6),a4 stacktiefe merken
move.w v_vec,a1 ca.gtstr
jsr (a1) string holen
bne.s erxit ? warnix
moveq #-15,d0 err.bp vorbereiten
subq.w #1,d3
bmi.s erxit ? nichts angekommen
move.w 0(a1,a6.l),d5 stringlänge
addq.l #2,a1 ptr auf erstes zeichen justiert
... string in eigenen speicher übertrgaen
... der vom Basic unabhängig sein sollte
moveq #0,d0 ordentlichen verlauf markieren
erxit add.l $5c(a6),a4 bv.ribas
move.l a4,$58(a6) RI-stack aufräumen
tst.l d0 flags setzen
rts
 
Rückgabe eines String in der schon bekannten Weise. Hier gilt
D4 = 1 für den Variablentyp String
D0 = 0 für fehlerfreien Verlauf
Der String selbst beginnt an geradzahliger Adresse mit dem Zähler in Wortlänge, es folgen die Zeichen.  Auffüllen bei ungerader Länge ist nicht nötig.
 

4.5.4.6 * Wertzuweisung

BP.LET $0120
Ggf. bereits beim Holen des Wertes sind die zugehörigen Pointer in die Name Table zu sichern. Für den ersten Parameter gilt A3 unverändert, sonst ist entsprechend um je 8 Bytes weiterzuzählen.

Nachdem im aufgerufenen Code alle nötigen Operationen erledigt sind, wird der neue Wert in der bereits beschriebenen Form im Arithmetik-Stack abglegt. Dies muß im Typen der zu ändernden Variablen erfolgen. Der Vector wird aufgerufen, nachdem BV.RIP auf den aktuellen Wert des Arithmetik-Stack eingestellt wurde, d.h. er muß auf die adressenmäßig untere Grenze der in Betracht kommenden Variablendaten gerichtet sein. Solange noch irgendwelche Posten übergeben werden sollen, ist dies stets der für BP.LET gültige Pointer auf den zu übernehmenden Variablenwert, der wirkliche Stackpointer, wenn er denn hiervon abweicht, muß erst vor der Rückkehr zum Basic wiederhergestellt werden.

A3 muß auf die Adresse des Ptr in die Name Table der zu besetzenden Variablen zeigen. Das ist derselbe Wert, wie er bei der Parameterübergabe an eine Procedur oder Funktion vom Basic her ankommt.

Rückkehr zum Basic kann aus einer Procedur unmittelbar danach erfolgen, weil dann zusätzlichen Angaben unnötig sind und alle "Aufräumarbeiten" automatisch geschehen.

D0 kommt mit der üblichen Fehlermeldung,
D1 - D3, A0 - A2 werden verdorben.
 

4.5.4.7 * Arrays (Variablenfelder)

Während für das Holen und Verändern einfacher Variabler das Betriebssystem eine ausreichende Anzahl Vectoren enthält, gibt es für Array-Variable keinerlei Unterstützung. Die Vectoren im MEM-Device aus IO2 sind so erweitert, daß sie, wenn eine Anzahl gleichartiger Parameter angefordert wird, auch Arrays annehmen. Es ist also durchaus möglich, dergleichen mit relativ geringem Aufwand nachzusetzen, seine Beschreibung würde hier aber weit über das Thema hinausgehen. Interessenten seien auf die Assemblerquellen zur IO2-Sammlung verwiesen.

Die Werte können einzeln nacheinander direkt gelesen werden.

Einer Basisadresse, auf der das erste Element der Anordnung zu finden ist, folgen Zusatzdaten über Lage und Anzahl der einzelnen Elemente. Sie sind in einem Kopfeintrag verzeichnet.
 

Langwort Ptr auf erstes Element rel. zu (BV.VVBAS(a6)+A6)
1. Wort Anzahl Dimensionen, ab 1 zählend
2. Wort höchster Index Dimension 0
3. Wort Index-Multiplikator (Postengröße) 
zur nächsten Datengruppe
...
(2n+2). Wort höchster Index der n-ten Dimension
(2n+3). Wort Index-Multilikator der n-ten Dimension
...
Strings haben besondere Angaben in der letzten Dimension:
vorletztes Wort maximale Länge, geradzahlig aufgerundet
letztes Wort immer Eins
 
Die einzelnen Elemente sind durchgehend regulär als Matrix abgelegt, die der Reihe nach Zeile für Zeile in den Speicher geschrieben wird, mehrere Dimensionen ebenfalls der Reihe nach. Der Index-Multiplikator gibt den Abstand in Bytes zum nächsten Element derselben Ordnung an, multipliziert mit dessen Länge.

Nur bei Variablenfeldern wird Integer-Zahlen platzsparend der tatsächlich benötigte Speicherbereich von zwei Bytes je Element zugeordnet.

Das Element Nr.0 der letzten Dimension eines String enthält seine Länge im Speicher. Liefert also das Ergebnis der Basic-Funktion LEN {22, dies ist genau so vorgesehen und dokumentiert, also keineswegs "bug" oder "undocumented feature"!}:

PRINT a$(1 TO ,1 TO ,0)!\
gibt die wirkliche Länge aller Eintragungen in das Array a$ aus, welches mit
DIM a$(erste_dim,zweite_dim,max_len)
definiert wurde.
 

4.5.5 ** Laufzeit-Konventionen

Einige wenige Regeln gelten innerhalb der vom Interpreter aufgerufenen Basic-Erweiterungen:

Beim Einsprung haben die Register den Inhalt

A3 erste Eintragung der Name Table einer Parameterliste.
A5 erste Adresse nach der letzten Eintragung dto.
A6 aktuelle Basisadresse für den Basic-Job
A7 User-Stackptr
Nach dem Aufruf befindet sich der Processor stets im User-Mode. Wurden Parameter angegeben, läßt sich deren Zahl aus der durch Acht geteilten Differenz von A3 und A5 ermitteln. Auf A6 sind alle Daten-Referenzen des Basic zu beziehen, seien sie nun vorhanden oder erst neu ermittelt worden. Dieses Register und die beiden Stackptr dürfen nur durch das System verändert werden, da der Basic-Bereich jederzeit zwischen zwei Befehlen - durch das System! - verschoben werden kann.

Kaum Einschränkungen gibt es in der Registerverwendung, die fast alle frei veränderbar sind:

D0 muß Null oder den Fehlercode zurückgeben.
D1 - D7, A0 - A5 dürfen beliebig verändert werden.
A6 Basisadresse des Interpreters, darf nicht verändert werden.
A7, A7' USER- und SUPERVISOR-Stackptr sollten unverändert bleiben.
Rückkehr muß im User-Mode des Processors erfolgen. Andernfalls bleibt neben den Möglichkeiten, die z.B. der C1-Monitor mitunter noch bieten kann, nur der RESET des ganzen Systems. Besonders nach solchen Irrläufern bereinigt der Kaltstart durch Reset nicht immer die gesamten Systemdaten, sodaß danach durchaus noch Reste der alten Fehlersituation stören können. Dann hilft nur Abschalten des ganzen Geräts für einige Sekunden - in seltenen Fällen, aber eher zusammen mit Floppy-Aktionen, kann es sogar erforderlich werden, die Batterie der GC kurzzeitig abzutrennen.

Übergang in den SUPERVISOR-Mode macht ein Programm "atomic", nicht teilbar, sperrt also den Scheduler, sodaß die anderen Jobs ruhen - vorausgesetzt, daß keine QDOS-Traps aufgerufen werden, die selber den Scheduler berühren. Zugleich gilt dann der Supervisor-Stack (in der Gegend der Systemvariablen). Nach {2} sind dort maximal 64 Bytes frei nutzbar, das wären etwa 16 Adressen oder Aufrufebenen. Auf den User-Stack greift der Processor dann als USP zu. Hier läßt sich nun wacker an den Registern herumspielen. Solcherweise wagemutig muß man aber dafür sorgen, daß besagte drei Adressregister am Ende richtig wiederhergestellt sind.
Es sei auf die Ausführungen zur Trap #0 verwiesen.

Der Systemstack heißt stets A7. Innerhalb einer residenten Basic-Erweiterung sind im USER-Mode dort bis zu 128 Bytes verwendbar - das sind z.B. nur 32 Adressen ! Was besondere Beachtung bei recursiven Verfahren erfordert. Auch, wer gewohnt ist, Daten im Stack zu übergeben, sollte hier wachsam sein. Eine Systemfunktion, mit Hilfe derer der nutzbare Bereich direkt erweitert werden könnte, ist nicht dokumentiert. Bei sorgfältiger Beobachtung der sonstigen Gegebenheiten können aber MT.ALBAS und MT.REBAS helfen (hoppala: NICHT im SMSQ, wie möchte es auch anders sein...).
 
 

4.5.6 ** Arithmetik-Speicher

Dieser relativ zu A6 adressierte Speicherbereich ist dem Basic-Job zueigen und in der Verwendung den Arithmetik-Routinen sowie der Parameterübergabe zugeordnet. Er kann davon unabhängig aber für jede beliebige Operation, bei Bedarf mit Hilfe der Trap #4, als Kurzzeit-Speicher eingesetzt werden. Mit Rückkehr zum Basic geht sein Inhalt verloren, schon allein, weil (bei Funktionen) sein Pointer entsprechend justiert werden muß.

Platz wird dort auf verschiedene Weise reserviert. Einmal sorgen die Vectoren zum Holen der Parameter dafür, daß genügend Platz vorhanden ist. Dann gibt es den Vector

BV.CHRIX $011A
dem in D1 die gewünschte Anzahl Bytes (geradzahlig!) mitzugeben ist. Außer, daß einige Register verdorben werden, bewirkt der aber nur, daß die Verfügbarkeit eines entsprechenden Bereiches sichergestellt wird, und u.U. der gesamte Basic-Speicher bedarfsgemäß verlagert wird. Ein Pointer darauf wird nicht gesetzt. Die Variable BV.RIP fixiert und schützt solche Bereiche, der neue Wert muß dazu aber erst dort eingetragen werden. Nur die Vectoren, die die Parameter holen, bewirken das automatisch.

Man schreibt etwa

moveq #8,d1 Platz für vier Integer
chrix move.l d1,-(sp)
move.w $011A,a1 mit bv.chrix
jsr (a1) sicherstellen
move.l $58(a6),a1  bv.rip
sub.l (sp)+,a1 justiert
move.l a1,$58(a6)  bereich fixieren
rts kein fehlercode (seit SMSQ...)
 
Nun stehen ab 0(a6,a1.l) acht Bytes sicher geschützt beliebigen Operationen zur Verfügung. Vor Rückkehr zum Basic noch folgende Aufrufe, auch der Vectoren zum Holen der Parameter, schließen sich mit den von ihnen belegten Bereichen dort lückenlos an. So besteht nach mehreren solcher Aufrufe ein geschlossener Bereich, in dem auch andere Arbeiten verrichtet werden können.

Freigabe erfolgt einfach durch Addition der reservierten Anzahl zu BV.RIP, die Einordnung übernimmt das System.
 
 

4.5.7 ** Arithmetik-Vector

Aufrufbar durch zwei Vectoren ist dem QL eine Sammlung Arithmetik-Routinen beigegeben, mit deren Hilfe sich verschiedene Rechenoperationen an f.p.-Zahlen durchführen lassen. Mit einer Tabelle wird dem Vector RI.EXECB eine Art Programm übergeben. Die weiter unten aufgeführten Codes veranlassen die einzelnen Operationen. Auch Adressierung eines eigenen Variablenbereichs ist möglich.
RI.EXEC  $011C führt eine einzelne Operation (D0) aus
RI.EXECB $011E dto., Operationen nach Tabelle (A3)+
Zu übergeben sind
D0 Op-Code bei RI.EXEC
D7 := 0 RI.EXP liefert sonst falsches Ergebnis {2}
A1 rel A6 Arithmetik-Stack
A3 abs(!) Liste der Op-Codes bei RI.EXECB
A4 rel A6 Basis des f.p.-Variablenpuffers
Zurück kommen
D0 evtl. mit dem Fehlercode ERR.OV
A1 dem Verlauf der Berechnungen angepaßt
Alle anderen Register bleiben erhalten.
Die Adresse A3 ist absolut zu übergeben (Angabe in {2} und {15} ist falsch!).

A1 arbeitet als Stack. Partner für die Speicherbefehle ist der Variablenspeicher, adressiert mit Offset zu A4. Beide Bereiche sind vorher auf geeignete Weise zu reservieren, etwa mit Hilfe des schon beschriebenen Vectors BV.CHRIX.

Zwei Arten Op-Codes werden erkannt. Neben den aufgeführten eigentlichen Rechenoperationen noch die Speicher-Befehle bezüglich eines Variablenspeichers: Ein Op-Code außerhalb des Bereiches 0 bis 48 gilt als Speicherbefehl: Geradzahlig zum Laden der adressierten Variablen in den Stack mit Predecrement, ungeradzahlig zum Ablegen aus dem Stack mit Postincrement an die adressierte Position im Variablenbereich. Die Adresse ist

A6 + A4.l + ((op-code OR $FF00) AND $FFFE).
Ein etwas anderes Vorgehen ist in {2} angegeben: Ein negativer Op-Code im Bereich von $FF31 bis $FFFF ruft zum Speichertransport auf. Die Operationen erfolgen, wie oben beschrieben. Die Adresse ist
A6 + A4.l + (op-code AND $FFFE)
In den untersuchten Roms (auch Gold-Card) wird die zuerst angegebene Methode benutzt. 34 Variable sind adressierbar.

Bei MINERVA gibt es hierfür einen wesentlich erweiterten Befehlssatz, dessen Beschreibung deren Handbuch entnommen werden kann. Die Codes liegen ungerade im Bereich von 1 bis 47 und sind hier kleingeschrieben verzeichnet.

Abgekürzt:
tos Top Of Stack, das unmittelbar zugängliche Element,
nos Next On Stack, das danach eingetragene Element.
top Top Of Stack nach einer Operation
 

Name Code Stack Funktion
RI.TERM 00   0 Beendet die Rechenoperationen.
ri.one 01 -6 Constante Eins ablegen
RI.NINT 02 +4 gerundete Integer
ri.zero 03 -6 Constante Null ablegen
RI.INT 04 +4 gekappte Integer
ri.n 05 -6 Byte in f.p.-Zahl -128 bis 127
RI,.NLINT 06 +2 f.p. in gerundete lange Integer 
ri.k 07 -6 (?) s. Handbuch
RI.FLOAT 08 -4 Integer nach f.p.
ri.flong 09 -2 lange Integer nach f.p.
RI.ADD 10 +6 top := tos + nos
RI.SUB 12 +6 top := nos - tos
ri.halve 13  0 top := tos / 2
RI.MULT 14 +6 top := tos * nos
ri.doubl 15  0 top := tos * 2
RI.DIV 16 -6 top := nos / tos
ri.recip 17  0 tos := 1 / tos
RI.ABS 18  0 tos := | tos |
ri.roll 19  0 tos: fp1 fp2 fp3 => tos: fp2 fp3 fp1  (?)
RI.NEG 20  0 tos := - tos 
ri.over 21 -6 tos: fp1 fp2 => tos: fp2 fp1 fp2
RI.DUP 22 -6 tos: fp1 => tos: fp1 fp1
ri.swap 23  0 tos: fp1 fp2 => tos: fp2 fp1
RI.COS 24  0 cosinus
RI.SIN 26  0 sinus
RI.TAN 28  0 tangens
RI.ASIN 30  0 arcus sinus
RI.ACOS 32  0 arcus cosinu
RI.ATAN 34  0 arcus tangens
ri.arg 35 +6 tos, nos: k * cis( arg )
RI.ACOT 36  0 arcus cotangens
ri.mod 37 +6 sqrt( tos^2 + nos°2 )
RI.SQRT 38  0 Quadratwurzel
ri.sqar 39  0 Quadrat
RI.LN 40  0 logarithmus naturalis (braucht ca 60 Bytes RI-Stack!)
RI.LOG10 42  0 lg
RI.EXP 44  0 Exponentialfunktion
ri.power 45 +2 nos(fp) ^ tos(Integer)
RI.POWFP 46 +6 nos ^ tos
Manchmal ist auch noch RI.MAXFP = 48 definiert.

Als Hilfsgrößen für die Assemblerschreibweise werden in {2} noch ein paar ziemlich sinnlose Pseudo-Operatoren für Stackbewegungen genannt, die zum Variablenoffset zu A4 zu addieren sind:

RI.LOAD equ 0
RI.STORE equ 1
Definiert man nun etwa
VAR1 EQU -6
hat man die Operationen
VAR1+RI.LOAD Laden der Variablen in den Stack
VAR1+RI.STORE Ablegen des ersten Stack-Elements
 

  oben : zurück : weiter : inhalt 

(count)