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 $0110Ihm 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 0 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 8Sind 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:
- 0 Verwendung
- 0 Variable nicht besetzt (use)
- 1 Ausdruck
- 2 Einfache Variable
- 3 Dimensionierte Variable
- 4 Procedur in Basic
- 5Funktion in Basic
- 6 Schleifenindex in REPeat
- 7 Schleifenindex in FOR
- 8 residente Procedur
- 9 residente Funktion
- 1 Typ
- 0 abhängig von der Verwendung:
- 3 Teilstring
- 4 Procedur in Basic
- 8 residente Procedur
- 9 residente Funktion
- sonst Fehler
- 1 String
- 2 f.p.-Zahl
- 3 Integer
- 2 - 3 Ptr
- auf den Namen innerhalb der Name List.
- sonst:
- -1 bei Ausdrücken, geschnittenen Strings,
Bytes 4-7 rel (BV.VVBAS(bp)) zeigen auf den Ausdruck.
- Andere negative Angabe bedeutet einen Ptr auf den Originaleintrag, von dem der Vorgefundene copiert ist.
- 4 - 7
- Basic-Definitionszeile im höherwertigen Wort (Bytes 6 und 7).
- sonst:
- Bei Ausdrücken der Ptr in den RI-Stack.
- Bei residenten Befehlen deren absolute Aufrufadresse.
- In allen anderen Fällen der Ptr auf den Variablenwert.
- -1 steht hier, wenn der Wert noch undefiniert ist.
Proceduren sind immer Typ 0.
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:
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.
CH.IO 0 Kanalidentifikation (ID des Qdos-Kanals)
-1 bei nicht mehr existierendem KanalCH.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 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
Typ in Bits 0 bis 3 wie oben beschrieben.
%.000.... 0 ohne Trennzeichen %.001.... 1 Komma %.010.... 2 Semicolon %.011.... 3 Bachslash ("\") %.100.... 4 Ausrufezeichen %.101.... 5 "TO" 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
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.CA.GTINT $0112Der Aufruf <getint> holt eine 16-Bit-Ganzzahl in Wortlänge nach D5:
v_vec equ $0112 Vector CA.GTINT v_len equ 2 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 anzugebenD4 = 3 für den Variablentyp IntegerEs sei die Zahl in D5.w gegeben. Dann folgt etwa:
D0 = 0 meldet fehlerfreien Verlauf
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.)
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.CA.GTFP $0114Die 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,5Dabei gibt es die Ausnahmen
Bit 21: 2^-10 = 1/1024 = 0,00097656Bit 31: -(2^0) = -1Die ganze Zahl
Alle Bits 0 = Mantisse der Zahl NullZahl = 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 anzugebenD4 = 2 für den Variablentyp f.p.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.
D0 = 0 meldet fehlerfreien Verlauf
4.5.4.4 * 32-Bit-Zahlen
CA.GTLIN $0118Fü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 $0116Strings 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 giltD4 = 1 für den Variablentyp StringDer 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.
D0 = 0 für fehlerfreien Verlauf
4.5.4.6 * Wertzuweisung
BP.LET $0120Ggf. 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.
Strings haben besondere Angaben in der letzten Dimension:
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 ...
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 mitDIM 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.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.
A5 erste Adresse nach der letzten Eintragung dto.
A6 aktuelle Basisadresse für den Basic-Job
A7 User-StackptrKaum Einschränkungen gibt es in der Registerverwendung, die fast alle frei veränderbar sind:
D0 muß Null oder den Fehlercode zurückgeben.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.
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.Ü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 $011Adem 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) ausZu übergeben sind
RI.EXECB $011E dto., Operationen nach Tabelle (A3)+D0 Op-Code bei RI.EXECZurück kommen
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.-VariablenpuffersD0 evtl. mit dem Fehlercode ERR.OVAlle anderen Register bleiben erhalten.
A1 dem Verlauf der Berechnungen angepaßt
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 istA6 + 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
Manchmal ist auch noch RI.MAXFP = 48 definiert.
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 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 0Definiert man nun etwa
RI.STORE equ 1VAR1 EQU -6hat man die OperationenVAR1+RI.LOAD Laden der Variablen in den Stack
VAR1+RI.STORE Ablegen des ersten Stack-Elements