Hallo Besucher, der Thread wurde 11k mal aufgerufen und enthält 60 Antworten

letzter Beitrag von BlondMammuth am

Vision: auf der klassischen Original-Hardwarebasis eines C64 von Grund auf neu geschaffene moderne System-Software in 16 KB ROM?

  • Toll, danke, Stephan! :-)


    Dann fange ich einmal mit einem Fazit aus der bisherigen Diskussion aus meiner Sicht an:


    An die Debatte thematisch angrenzend:

    Zur Vision selbst:

    • Es gelten nur Lösungen, die es tatsächlich den Usern erlauben, von Anfang an mit der Maschine allein und historisch etablierten Peripheriegeräten, ohne weitere äussere Hilfsmittel (wie PC-Cross-Compiler etc.), einigermassen simpel (also vergleichbar dem on board Basic-Interpreter und -Editor) loszulegen.
    • Es ist aber ausdrücklich erlaubt und erwünscht, dass so eine Lösung mit externen Hilfsmitteln / Erweiterungen etc. gut zusammenarbeitet, wenn man das wünscht.

    Was bisher geschah:


    Die Vermutung steht im Raum, dass eine Virtuelle Maschine viel Platz sparen könnte.


    Eine Diskussion ist am Laufen, ob diese Maschine ev. so gestaltet werden könnte, dass Funktionalität schnell und einfach "compiliert" werden kann (also so, dass der Compiliervorgang sich auf kleine Einheiten bezieht, und in der Größenordnung ungefähr so verhält wie das Übernehmen einer BASIC-Zeile). Das ist mit ein paar konzeptuellen Schwierigkeiten verbunden: Z.B. was macht man mit den Identifiern? Wie behält man Formatierungsinformation bei (z.B. Zeilenvorschub, Tab, Einrückungstiefe, etc.). Wie bewahrt man Kommentare? Dazu gibt es zwar Vorschläge, aber ob die machbar sind, ist nicht heraussen.


    Eine weitere Frage ist, ob so ein Byte-Code allgemein und effizient genug sein könnte, damit es sich auszahlt, weitere Compiler zu schreiben, die ihn als Zielsprache verwenden. Wenn man das macht, stellt sich weiter die Frage, ob man solche Compiler bereits auf dem C64 laufen lassen könnte, und unter welchen Bedingungen.


    Sehr wichtig ist die Frage, ob diese VM schnell genug sein könnte, um Teile der Kernal-Funktionalität darin auszudrücken, und wenn ja, wo die Grenze sein müsste.


    Falls ich nichts vergessen oder falsch verstanden habe, wäre das soweit der Stand der Gedanken.

  • Hm...also ich hääte gerne eher mehrere und dafür spetiallisierte kernels -die ich dann auf ner 30Fach Umschaltplatine einsetze und immer den Kernel für den enrsprechenden Usecase verwende. Was ich mir da vorstellen könnte:

    • Ein SW-Entwicklungs Kernele (eine für BASIG, einen für ASM)
    • Einen Kernel Entwicklungskernel :)
    • Einen Internett Browser Kernel
    • Einen BBS-Kernel
    • Einen SimonBasicExtended Kernel
    • Einen Kernen mit C64-Scratch (ja das wäre der Traum fürs Coder Dojo)
    • Einen Sound Entwicklungskernel
    • Einen DOS-Kernel
    • Einen IO-Kernle (spetialisiert auf CIA-IO anwendungen wie Robotersteuerungen).
  • Könnte man diese Anforderungen theoretisch in einen gemeinsamen und einen jeweils spezialisierten Teil aufspalten? Denn dann könnte man diese Teile eben nicht mehr als "Kernel", sondern als entsprechende Treiber oder Libraries konzipieren, die man bei Bedarf nachlädt oder einblendet.


    Bzw. - was ich nicht weiß - ist SimonsBasicExtended z.B. mit einem neuen Kernal ausgestattet? Ich kanns mir vorstellen, aber eigentlich sollte man ja annehmen, dass in erster Linie der BASIC-Teil verändert ist?


    Wobei, wie mir grade auffällt, man die mit dem angepeilten Konzept ja alle nachbauen müsste, denn die ursprünglichen Implementierungen würden kaum mehr ins (neue) Konzept passen.

  • Hm...also ich hääte gerne eher mehrere und dafür spetiallisierte kernels -die ich dann auf ner 30Fach Umschaltplatine einsetze und immer den Kernel für den enrsprechenden Usecase verwende. Was ich mir da vorstellen könnte:

    • Ein SW-Entwicklungs Kernele (eine für BASIG, einen für ASM)
    • Einen Kernel Entwicklungskernel :)
    • Einen Internett Browser Kernel
    • Einen BBS-Kernel
    • Einen SimonBasicExtended Kernel
    • Einen Kernen mit C64-Scratch (ja das wäre der Traum fürs Coder Dojo)
    • Einen Sound Entwicklungskernel
    • Einen DOS-Kernel
    • Einen IO-Kernle (spetialisiert auf CIA-IO anwendungen wie Robotersteuerungen).

    Das ist entweder "der durchsichtige Versuch, die 16 KB - Schranke zu umgehen" oder ein satirischer Beitrag, den ich natürlich hochwillkommen heisse :)

  • Ich schreibe einmal eine Grund-Idee her, ob sie brauchbar ist, mögen andere entscheiden:


    Ich würde als VM-Sprache eine pseudo-funktionale Sprache vorschlagen, wobei sich fast alles auf dem Daten-Stack abspielt. Dabei liegen Daten- und Return-Stack irgenwo im RAM (nicht der System-Stack!), was durch zwei Stack-Register (in der ZP) dargestellt wird.


    Jede Operation bezieht sich im einfachsten Fall direkt auf die obersten Stack-Elemente, kann aber auch auf andere Stack-Elemente addressiert werden. Es wirft die erste Frage sich auf: Wieviele Bits gönnen wir dieser Addressierungsform? Ich würde vorschlagen, das mit der Größe des Stack-Fensters zu kombinieren, das günstigerweise auch in der ZP liegt.


    Mir erscheinen intuitiv 16 oder 32 Bytes für dieses Fenster als vernünftig, und ich würde mir den Mechanismus so vorstellen: Der Stack wird in einem geschlossenen Kreislauf befüllt, und wenn neue Elemente gebraucht werden, werden 8 Bytes in einem auf den Stack "draussen" geschrieben, und im ZP-Fenster somit zum Überschreiben freigegeben. Sollte der Stack wieder geleert werden, werden sie wieder von draussen zurück geholt, und das immer in je 8(vielleicht auch 16?) Bytes-Portionen.


    Eine schnelle Stack-Adressierungsart wäre dann vermutlich mit 8 byte Reichweite sinnvoll. Eventuell mit 16. Darüber gehört noch nachgedacht. Jedenfalls könnte man so lokal fast immer für ZP-Zugriffe garantieren.


    Man braucht natürlich weitere Addressierungsmodi: Für lange Stack-Zugriffe, für absolute Speicher-Zugriffe inklusive jeder Indizierungsvarianten, und für virtuelle Speicher-Zugriffe inklusive Indizierungsvarianten. Letztere dienen dazu, Paging zu ermöglichen, und jedem Programm quasi seinen eigenen Adressraum zu geben.


    Diese Addressierungsmodi bräuchten aber nicht alle Befehle betreffen. Es würde ausreichen, wenn man damit bloß Werte in das lokale Stack-Fenster holen kann und von dort wieder hinausschreiben. Eventuell ist es aber zwecks Optimierung sinnvoll, die Befehle trotzdem zu erlauben, die der 6510 schon selbst mit interessanten Addressierungsmodi anbietet, um die nicht unnötig zu verlangsamen.


    Im Stackfenster spielen sich fast alle einfachen Operationen ab, also Addition, Multiplikation, bitweise Verknüpfungen, etc. Dabei stellt sich sofort die Frage, ob man dafür noch extra Register mit absoluter Addresse benutzen sollte, umkopieren, und dann die Operation durchführen, oder ob das ZP indiziert auch noch ausreichen würde. Vielleicht kann da jemand ein Machtwort sprechen? Ich vermute fast, dass eigene Register bei größerer Byte-Anzahl den Geschwindigkeitsvorteil bieten. Bei Float-Operationen geht es sowieso nicht anders.


    Das bringt mich zur Frage der unterstützten Datentypen. Ich würde tatsächlich vorschlagen, Typen bis zu 64 bit zu unterstützen (was dann ein 32-Byte-Stack-Fenster sinnvoll macht). Dann hätte man bis zu long und double-Typen auch drinnen, wobei ich vermute, dass double-Operationen ziemlich teuer wären. Der Flaschenhals wären dabei wohl am ehesten die Umwandlungs-Operationen binär-dezimal und umgekehrt. Das würde vermutlich einiges an Programmlänge für den Bytecode-Interpreter bedeuten.


    Der wirklich riesige Vorteil wäre, dass man damit eine Menge moderner Sprachen zumindest rudimentär auf diese VM übersetzen könnte (natürlich per cross-Compiler, denn die entsprechenden Compiler wären für den C64 schlicht und einfach viel zu komplex).


    Jeder Op-Code bekommt neben der Addressierungsart hineincodiert, wie groß seine Operanden sind, und weiß daher, wo auf dem Stack er sie findet. Soweit nötig, werden die in die entsprechenden Register kopiert, die entsprechende Routine aufgerufen, und das Ergebnis wieder abgeholt. Es fragt sich, wieviele Bit man dafür braucht. Ich vermute einmal 2-3 bits für die Addressierungsart, und weiters 2 oder 3 bits für die Operandengröße (1,2,4, ev. 8 müssen dargestellt werden). Macht zwischen 4 und 6 bits alleine dafür. Wir werden also sicher 2 Bytes pro Op brauchen. Man hätte sonst maximal 4 bits für die Operation selber, das sind 16 mögliche Instruktionen. Klingt extrem mager, würde ich sagen. Vielleicht gehts auf mit einem Byte, was einen entscheidenden Geschwindigkeitsvorteil und auch Vorteil für die Programm-Größe bedeuten würde, aber ich bin sehr skeptisch. Eventuell kann man eine Mischung hernehmen.


    Die anderen Addressierungsarten benötigen wahlweise bis zu 2 Bytes mehr, wobei man 1 Byte für den Stack hernehmen könnte, und für die absolute Zero-Page-Addressierung (man könnte das ZP-Konzept eventuell ins virtuelle übernehmen, und auf den Stack genauso, also die ersten 256 Bytes vielleicht billiger addressieren. Aber das sind Details). Für die indirekte Addressierung ob absolut oder virtuell) wäre sowieso das oberste Stack-Element als Addresse zuständig.


    So, hier unterbreche ich erst einmal, und mache morgen weiter. ;-)


    (.. und fürchte schon die Reaktionen derer hier, die wirklich wissen, wovon sie schreiben!) :nuss:

  • Mir erscheinen intuitiv 16 oder 32 Bytes für dieses Fenster als vernünftig, und ich würde mir den Mechanismus so vorstellen: Der Stack wird in einem geschlossenen Kreislauf befüllt, und wenn neue Elemente gebraucht werden, werden 8 Bytes in einem auf den Stack "draussen" geschrieben, und im ZP-Fenster somit zum Überschreiben freigegeben. Sollte der Stack wieder geleert werden, werden sie wieder von draussen zurück geholt, und das immer in je 8(vielleicht auch 16?) Bytes-Portionen.

    Das klingt so, als ob du Registerfenster neu erfinden willst.


    Zitat

    Diese Addressierungsmodi bräuchten aber nicht alle Befehle betreffen. Es würde ausreichen, wenn man damit bloß Werte in das lokale Stack-Fenster holen kann und von dort wieder hinausschreiben.

    Eine Load/Store-Architektur ?

  • Mich würde interessieren, ob die Opcodes deiner "Stackmaschine" Zweiadreß-Instruktionen oder Dreiadreß-Instruktionen sind.
    Ersteren Falls wäre z.b. nur folgendes möglich:
    A = A+B


    letzteren Falls auch solches:
    A = B + C


    Entschuldige bitte meine inkompetente Nachfrage, aber ich verstehe nur diese randständigen Aspekte :)



    • Debatten zur Compilertechnik und zum Programmiersprachendesign im Hinblick auf das existierende 6510-System
    • User-Präferenzen zur Nutzung des heutigen aber auch eines zu "erfindenden" Systems


    Ich kenne zuwenig "Paradigmen" von (aktuellen) Programmiersprachen und würde bei den Präferenzen anfangen nach "Bereichen" zu sortieren:


    - wieviel Arithmetik brauche ich? wieviel "höhere Mathematik"? transzendente Funktionen? 3D ? Komplexe Zahlen? Welche Genauigkeit?


    - welche Datentypen, wenn überhaupt? Genügen "Variant"-Typen die sich ihrem Inhalt anpassen?


    - Welche Zeichenketten? fixe Strings oder dynamische oder PChar oder Char? Zeichenketten als Resource (DATA in basic)


    - Kontrollstrukturen, Blöcke, Flußsteuerung (wenn nicht gerade "Funktionale Sprache!") FOR .. NEXT Entsprechungen; REPEAT ..


    - I/O Funktionen Konsole, Bildschirm, Massenspeicher-Einbindung


    - Schnittstelle zur Memory-Verwaltung (Reservierter Speicher, Heap-Verwaltung..)


    - Hierarchien , Units, Includes ...


    Was ist wünschenswert - was realistisch. Mit welchen Konzepten kommt man mit wenig Aufwand am weitesten.... Zu den genannten kategorien hab ich schon gewisse Vorstellungen, muss mir aber erst zurechtlegen was mir sinnvoll erscheint.

  • Das klingt so, als ob du Registerfenster neu erfinden willst.

    Eher will ich sie adaptieren. Erfunden sind sie ja schon, wie du richtig bemerkt hast. ;-)

    Eine Load/Store-Architektur ?

    Soweit war ich noch gar nicht. Ich überlege mir halt, was auf die Schnelle sinnvoll wäre, und warte das Feedback ab. Obs dann genau das wird oder nicht, wird man sehen, wenns hinten rauskommt. Ich habe keine Berührungsängste mit bekannten Konzepten, aber auch keinen Drang, mich ihnen sklavisch zu unterwerfen.

    Entschuldige bitte meine inkompetente Nachfrage, aber ich verstehe nur diese randständigen Aspekte

    :D "Solls ein Diesel oder ein Benziner werden?" "Ruhe, ich tüftle grad an der Anzahl der Räder! Oder sollen wir lieber Tragflächen nehmen?"!


    Aber konkret und ernsthaft: Ich bin eigentlich von einer Stack-Architektur ausgegangen, also sollte es vermutlich eher als oberstes Stack-Element dazu gepusht werden. Das wäre dann wohl eine 2-Address-Architektur. Andererseits - trotzdem danke für die Frage, denn eigentlich muss das für die anderen Addressierungsarten ja nicht unbedingt gelten. Fragt sich, ob man irgendwas an Performance gewinnt, wenn man eine 3-Address-Architektur verwendet. Vermutlich ein wenig, weil man Berechnungsergebnisse in manchen Fällen nicht mehr auf dem TOS zwischenspeichern würde, aber dann wären manche Instruktionen automatisch um 2 Bytes länger. Gut, dafür erspart man sich dann je eine STORE-Instruktion. Hm .. ehrlich gesagt, da bin ich noch gar nicht so schlüssig. Aber mehr als ein Brainstorming ist es ja eh noch nicht. :gruebel

    Ich kenne zuwenig "Paradigmen" von (aktuellen) Programmiersprachen und würde bei den Präferenzen anfangen nach "Bereichen" zu sortieren:

    Bin dafür! Und was Paradigmen betrifft, stricke ich die am liebsten selber! ROTFL Na, sagen wir so: Für die reine Lehre reicht in fast allen Fällen die HW ohnehin nicht aus, also nehmen wir einfach, was sich als praktisch anbietet, würde ich sagen.


    Die Einteilung ist ein berechtigtes Anliegen. Soweit war ich noch nicht. Bei mir hat sich oben erst beim Schreiben rauskristallisiert, dass es sich um VM und Bytecode handelt. Es ist ja nochnicht viel mehr als "laute Selbstgespräche" aka "Brainstorming".

    Zu den genannten kategorien hab ich schon gewisse Vorstellungen, muss mir aber erst zurechtlegen was mir sinnvoll erscheint.

    Bitte nur zu! Und wenn sich was nachträglich als nachteilig erweist, auch kein Problem! Das wird noch oft genug passieren. :-)


    Ich habe übrigens auch einige Ideen dazu, die kommen noch. Ich werd gleich im Anschluss was zur Sprache posten, wie sie in meinem Kopf bisher entstanden ist.

  • Zur Sprache


    Das wäre eine Sprache, die der Stack-Orientierung so weit wie möglich angepasst ist.


    Ausdrücke


    Ich nehme einmal an, ich habe Präfix-Ausdrücke (die kann man von "höheren" Sprachen aus als Infix-Ausdrücke darstellen, aber für die limitierten Edit-Fähigkeiten ist Präfix zur Umwandlung leichter).


    Welche Operationen kann es im Code geben? Die üblichen Arithmetischen und Bitweisen Operationen und user-definierten Funktionen einerseits, und Konstanten andererseits. Wir hätten dann im Code auch Konstanten drin, die so, wie sie sind, auf den Stack gepusht werden. Typ und Größe müsste man entsprechend syntaktisch kenntlich machen (wie in BASIC mit "%" und "$", nur eben für Konstanten, und dabei müsste die Größe auch explizit irgendwie vermerkt werden). Das will noch durchdacht werden. Nehmen wir für den Augenblick ganz normale int oder sonstige Werte an:


    Konstanten kann man dann quasi von rechts nach links auffassen, und in der Reihenfolge füllen sie den Stack auf. Also z.B.:


    1 X 23 "Hallo"


    wäre ein Ausdruck, der diese Werte (beim String eine Referenz) auf den Stack schreibt, wobei 1 der Top of Stack wäre. Das X würde für eine Variable stehen, deren Inhalt dorthin kopiert wird.


    Dann kann man irgendwelche Operationen drauf anwenden, und weitere Elemenete erhalten. , wie z.B. in


    + 1 X 23 "Hallo"


    Da würde


    (1+X) 23 "Hallo"


    rauskommen (also wenn X=3, wäre das .. 4 23 "Hallo").


    Um auch Funktionen wie "first order citizens" (Bürger erster Klasse) behandeln zu können, kann man Beistriche verwenden. Bei Konstanten ergeben die noch keinen Unterschied, also wären


    1 X 23 und 1, X, 23


    dasselbe. Der Unterschied wird durch Funktionen sichtbar:


    + 1 X, 23 ==> 1+X, 23


    aber


    +, 1, X, 23 ==> +, 1, X, 23


    Das "+" wird nicht evaluiert. Somit hätten wir mit dem Beistrich einen "Schutz" des Stacks symbolisiert. Allerdings kann es sein, dass man einer Funktion eine andere Funktion als ein Argument und noch ein paar übergeben möchte, z.B: F braucht 3 Argumente. Würde man dann schreiben:


    F G, 1, X


    würde das nicht funktionieren, weil F dann 1 und X gar nicht mehr übernimmt. Um das zu ermöglichen, benutzen wir runde Klammern:


    F( G, 1, X )


    was soviel bedeutet wie "die Ausdrücke innerhalb der Klammer kommen sich nicht in die Quere, aber F konsumiert sie alle trotzdem".


    Was kommt aber heraus, wenn man doch nur F G, 1, X hinschreibt?


    Das Ergebnis von F G ist eine Funktion, die noch zwei andere Argumente verlangt, denn eines hat sie ja schon. Das ist als "currying" bekannt, die Möglichkeit, Funktionen teilweise mit Argumenten zu versorgen. Umgekehrt kann es sein, dass man einer Funktion zuviele Argumente gibt. Dann bleiben die überzähligen Argumente auf dem Stack stehen.


    Soweit zu den Ausdrücken, die man bilden kann. Dazu gibt es sicher noch mehr zu sagen, und das nächste wären Zuweisungen, Deklarationen, Definitionen, etc. - mehr dazu im nächsten Posting.

  • Lokale Variablen, Deklarationen, Zuweisungen


    Nehmen wir das Stack-Modell aus den Ausdrücken weiter her, und bauen lokale Variablen ein. Die Syntax ist jetzt nicht wichtig, also nehme ich einmal die aus C-ähnlichen Sprachen (vielleicht wirds dann eine eher Pascal-artige, das soll jetzt nicht zu Konflikten führen):


    Nehmen wir ein paar Deklarationen (und gleich auch Definitionen) vor:


    int16 i=3, j=5;
    int32 x=100 000;
    char c='c';


    Wie sieht der Stack jetzt aus? Diese Variablen haben ihre Plätze:


    ==> ( c='c', x=100 000, j=5, i=3)


    Wie man sieht, liegen sie quasi verkehrt auf dem Stack, was auch logisch ist: Je älter, desto weiter unten/hinten auf dem Stack.


    Und dann schreiben wir einen Ausdruck wie vorher hin:


    +(i, j)


    Wie sieht der Stack jetzt aus? Er enthält auf jeden Fall die vorher definierten Variablen, und danach den neuen Wert:


    ==> ( 8, c='c', x=100 000, j=5, i=3)


    Nutzen wir diesen Ausdruck in einer Zuweisung:


    x := * x - i j;


    Diese Anweisung, auch den Ausdruck, muss man von links nach rechts lesen. Nehmen wir "loc" als den Zeiger auf das Ende der Deklarationen, und "top" als den Zeiger auf Top of Stack, dann kommt (in Pseudo-Code) etwa das heraus:


    push16 (loc+7); push16 (loc+9); sub1616; push32 (loc+1); mult3216; pull32 (loc+1);


    Oder, wenn man die Addition mit offset addressieren kann, wäre der Code erheblich kürzer:


    sub1616 (loc+7) (loc+9); mult3216 (loc+3) (top); pull32 (loc+1);


    Diese Variante hätte den Vorteil des kürzeren Codes, und wäre auf jeden Fall vorzuziehen, wenn man eigene Register für die Berechnungen einführt. Denn dann erspart man sich eine Menge Kopiererei.


    Das Ergebnis wäre dieser Stack:


    ==> ( c='c', x=200 000, j=5, i=3)


    Es ist eine offene Frage, ob man wirklich zwei Indizes (top, loc) verwendet, oder die entsprechenden Indizes einfach dazu zählt, wobei die zweite Variante den Nachteil hätte, dass man nicht beliebig (z.B. in Schleifen) pushen kann, weil statisch bekannt sein müsste, wie der Stack an einer bestimmten Stelle des Programms angewachsen ist. Darüber darf noch nachgedacht werde. Sicher ist, dass "top", und ggf. "loc", Registern in der virtuellen Maschine entsprechen müssen.


    Von der Sprache her gesehen:


    Eine Anweisung kann aus einem Ausdruck allein bestehen. Sobald sie jedoch mit Semikolon abgeschlossen wird, ist der Top of Stack ab da wieder wie vorher, d.h. was auch immer man durch irgendwelche Operationen oder Funktionen auf den Stack dazu "gepusht" hat, ist dann (zumindest offiziell) verloren.


    Das entspricht weitgehend der ganz normalen Semantik, die man von Programmiersprachen generell gewohnt ist. Will man das verhindern, muss man den Ausdruck irgendwie im selben Statement (Anweisung) weiterverwenden.


    Tupel


    Tupel sind im Grunde mehrere Werte hintereinander, die man als einen betrachtet. Sie werden mit spitzen Klammern begrenzt. Die nächste Operation konsumiert sie alle. Z.B., wenn wir annehmen, wir wollten alles an eine bestimmte Addresse im Speicher schreiben. Nagelt mich jetzt nicht auf die Schreibweise fest, das kann sich noch ändern. Ich nehme inzwischen die C-Syntax für die Zahl, aber eine andere für die Pointer-Operation. Das A^ heißt sinngemäß: "Nimm diese Zahl als Ziel-Addresse für die Zuweisung", und ich erlaube casts im C-Stil, um den Typ anzugeben (das ist rein improvisiert und kann sich auch noch ändern):


    0xD000^ := <(int8)47,(int16)11,(int16)4711,(int8)08,(int8)15,(int16)0815>;


    Mit dieser Operation werden sämtliche Zahlen innerhalb der Klammern, wie sie auf dem Stack liegen, ab dem Speicherbereich 0xD000 abgelegt. Das wäre gleichbedeutend mit:


    0xD000^ := (int8)47
    0xD001^ := (int16)11
    0xD003^ := (int16)4711;
    0xD005^ := (int8)08;
    0xD006^ := (int8)15;
    0xD007^ := (int16)0815;


    Ebenso kann man diese nutzen, um mehrern Variablen etwas zuzuweisen, was woanders liegt. Nehmen wir an, wir hätten an einer Adresse A verschiedene Werte hintereinander stehen, die wir in Variablen füllen wollen. Wir könnten das so tun:


    <x, i, j, c> := A^;


    Damit würde das, was in A steht, aufgespalten in Werte, die den Bytelängen der angegebenen Variablen entsprechen, und diese Variablen damit befüllt.


    Übrigens könnte man diesen Mechanismus auch ähnlich wie "Data/Read" nutzen, wenn man ihn ein wenig aufbohrt. Er hat jedenfalls den Sinn, sich Kopier-Schleifen generell zu ersparen. Somit wird ihm sinnvollerweise auch eine Instruktion der virtuellen Maschine entsprechen.


    Im den nächsten Postings will ich einerseits auf globale Definitionen eingehen, die im Rahmen so eines Programmiersystems eine ganz besondere Rolle auf mehrere Weisen spielen, andererseits auf Kontrollstrukturen. Schauen wir einmal, zu welchem Thema meine Gedanken schneller sind!

  • Seh ich das richtig, dass bei deinen Tupeln alle Werte mit ihren zugehörigen Bezeichnernamen wie eine Hash-Tabelle auf dem Stack liegen? gar mit "=" als ASCII-Text als dritte Spalte dazwischen?


    Irgendjemand muss sie aber letztlich doch dahin kopiert haben.


    Zu sagen, um Kopierorgien zu sparen, dann aber pro Variable drei Kopiervorgänge "im Hintergrund" zu erfordern, scheint mir noch nicht als der große Gewinn. Aber vielleicht kannst du hier Klarheit schaffen?


    Mir leuchtet nicht ganz ein, wieso ein auf Stack basierendes Konzept ausgerechnet für den 6502-Prozessor besonders geeignet sein soll, der sich doch durch einen kurz & kleingestutzten Mini-Stapel von 256 Bytes vom Vorgänger 6800 unterscheidet!


    OK, die Evaluierung von Ausdrücken ist besonders "stack-affin". Ich habe vage in erinnerung, dass Klammern, Punkt-Vor-Strich-Rechenregeln usw. am besten mit einem Stack gelöst oder soll man sagen, umschifft werden können. Auch bei der Optimierung. Irgendwo wird ein Baum "traversiert" und dann kann man auf wundersame Weise konstante Glieder rausziehen oder Dead-ends beseitigen etc.


    Aber generell hab ich mal gelernt, dass das meiste was in der Programmiererei rekursiv geschrieben werden kann, zu einer iterativen Lösung umgewandelt werden kann. (auch das Kopieren von Werten auf einen Software-Stack verursacht Aufwand, so um die 8 Zyklen pro Wert plus Schleifen-Overhead).


    Und dabei finde ich, sollte eine auf einem beschränkten 8-bit-Rechner lauffähige Hochsprache die Benutzerin den / Benutzer ermutigen ins Iterative zu gehen und das Rekursive eher in die 2. Reihe zu plazieren.

  • Danke für die Fragen! :-)



    Seh ich das richtig, dass bei deinen Tupeln alle Werte mit ihren zugehörigen Bezeichnernamen wie eine Hash-Tabelle auf dem Stack liegen? gar mit "=" als ASCII-Text als dritte Spalte dazwischen?

    Nein, sondern als reine Bytes auf dem Stack. Die Namen sind zwar bekannt, solange man an dem Programm interaktiv bastelt, werden aber erstens extra verwaltet (nicht im Code, nicht auf dem Stack), und zweitens irgendwann, wenn man es zu reinem Bytecode verdichtet, zusammen mit vielen anderen Details, die zum Ablauf nicht nötig sind, entsorgt. Da kommt noch mehr dazu. Jedenfalls stehen die Namen für Variablen, und diese für ihre Positionen und Typen. Damit hat sich auch die Frage mit der Kopier-Orgie geklärt, nehme ich an?


    Mir leuchtet nicht ganz ein, wieso ein auf Stack basierendes Konzept ausgerechnet für den 6502-Prozessor besonders geeignet sein soll, der sich doch durch einen kurz & kleingestutzten Mini-Stapel von 256 Bytes vom Vorgänger 6800 unterscheidet!

    Die Antwort findet sich im Posting von 23. Mai 2017, 23:36, und zusammengefasst ist es so, dass es zwei viel größere und flexiblere Stacks gibt, einen Return- und einen Daten-Stack, wobei der Daten-Stack in einem Fenster in der ZP gespiegelt wird. Der interne 6510-Stack dient nur der VM zur Abarbeitung oder wird eben von reinen Maschinenprogrammen weiterhin genutzt.


    OK, die Evaluierung von Ausdrücken ist besonders "stack-affin". Ich habe vage in erinnerung, dass Klammern, Punkt-Vor-Strich-Rechenregeln usw. am besten mit einem Stack gelöst oder soll man sagen, umschifft werden können. Auch bei der Optimierung. Irgendwo wird ein Baum "traversiert" und dann kann man auf wundersame Weise konstante Glieder rausziehen oder Dead-ends beseitigen etc.

    Könnte man theoretisch überlegen, nur gehts hier um die on-Board-Programmierung, was bedeutet, dass die Umwandlung von lesbaren in Byte-Code besonders schnell und kurz sein sollte, also auch möglichst wenig Aufwand erfordern soll. Ich denke, da wären solche Optimierungen bereits gefährlich nahe an der Verschwendung. Allerdings - durchdenken schadet nichts. Vielleicht ist die Idee auch machbar. Wäre ein Alternativ-Vorschlag.



    Aber generell hab ich mal gelernt, dass das meiste was in der Programmiererei rekursiv geschrieben werden kann, zu einer iterativen Lösung umgewandelt werden kann. (auch das Kopieren von Werten auf einen Software-Stack verursacht Aufwand, so um die 8 Zyklen pro Wert plus Schleifen-Overhead).

    Definitiv. Dem entspricht in dem Fall die Tatsache, dass die Ausdrücke praktisch nur umgedreht werden müssen, um entsprechenden Code zu ergeben. Oder habe ich diese Anmerkung falsch verstanden? Meinst du den Befehl zum Kopieren von größeren Bereichen? Gut, den kann man ja in der VM beliebig implementieren.



    Und dabei finde ich, sollte eine auf einem beschränkten 8-bit-Rechner lauffähige Hochsprache die Benutzerin den / Benutzer ermutigen ins Iterative zu gehen und das Rekursive eher in die 2. Reihe zu plazieren.

    Gut, jetzt hast du mich ein wenig verloren. Wir sind noch gar nicht bei Schleifen etc, und auch nicht bei Rekursionen. Dahin komme ich erst. Aber vielleicht verstehe ich auch grade wirklich nicht, worum es dir geht. Vielleicht klärt sich das im Folgenden, vielleicht kommt aber eine Stelle, an der du mir es konkret erklären kannst.

  • Ich konnte deinen Ausführungen leider nicht entnehmen, an welcher Stelle im Programmiersystem der besagte "Stack" überhaupt platziert d.h. "aufgehängt" ist.


    1) In der Auswertung von Benutzereingaben entsprechend Basic-"Direktmodus"?
    oder
    2) In der Entsprechung zur basic-Routine "EVAL" = Ausdrücke auswerten? (Ausdrücke= math. Berechnungen!! vulgo: Formelparser)
    oder
    3) Beim Parser des allgemeinen Quelltexts, zu Compilierzwecken?
    oder
    4) Beim Tokenisierer des allgemeinen Quelltexts, zu Interpreterzwecken?


    Pardon, deine Ausführungen scheinen mir mit meinem beschränkten Hintergrundwissen in mancherlei Hinsicht etwas wolkig, so dass sich bei jeder vermeintlichen oder tatsächlichen Mehrdeutigkeit mein Gehirn in je 2 Tochterprozesse spaltet, die versuchen die jeweilige Bedeutungsmöglichkeit habhaft zu werden, nur um rekursiv sich in immer neuen Deutungsprozessen weiter aufzuspalten und zu verlieren.


    Besonders schnell hakt es aus, wenn du von Tupeln sprichst.
    Ich halte es mit Verlaub für akademisch, ein Konzept zu propagieren, das zum Weglassen von Parametern ermutigt (nach dem Motto: huch, wie interessant, schauen wir mal womit wir die CPU noch alles piesaken können ... der Stapelzeiger dekrementiert schon wieder unerwartet ... wir brauchen Exception handling --> der arme 6502 muss jetzt auch noch einen Pentium Pro emulieren...), wo die aufgerufene Routine auf korrekte Übergabe von all ihren Argumenten angewiesen ist.


    diese Art "Tupel" dann als eine Art Pipe(line) zu benutzen um irgendeinen Speicherbereich zu füttern bzw. vollzumachen , erscheint mir erstens als (versteckter) Kopiervorgang und zweitens sinnfrei, wenn man annimmt dass der Tupel rechts ja bereits offenkundig eine geordnete Liste IST und ganz sicher bereits im Speicher vorliegt und nicht erst dazu gemacht werden muss (was wie gesagt nur einen unnötigen Kopiervorgang vom Quelltext in den "Stack" darstellt).


    Auf Anhieb würde ich - in Pascal-Notation - sagen
    $D000 := Addr( Tupel)


    ohne Dereferenzierer "^" links,
    und der "Kas ist gegessen" ;)


    Was spricht dagegen , in dem Fall einfach einen Zeiger auf den Beginn der Tupel-Liste im Quelltext weiterzureichen?


    In Delphi-Pascal gibt es das Konstrukt "Resourcestring".
    Da hat man im Datensegment der EXE-Datei einen Haufen Variablen - häufig strings, kann man aber bei Bedarf sicher anderes unterbringen - und da bietet die Laufzeitumgebung die Möglichkeit, einen Zeiger dorthin zu bekommen.


    Mir kommt es unnötig kompliziert,und mit Verlaub akademisch vor, hier Kopiervorgänge zu "installieren" die nur nicht solche heißen dürfen.. ich kann mich natürlich irren, oder sehe das zu pragmatisch oder bin in meiner Beschränktheit unfähig, die Weisheit und Eleganz hierin zu erblicken!

  • Vorab:
    Alle Überlegungen über irgendwelche Programmiersprachen verlangen zwingend nach einem anderen Ansatz zur Programmtexteingabe. Ein Zeileneditor mag für BASIC funktionieren, da Programmtext und Tokenstrom direkt aufeinander abgebildet werden können mit dem Nachteil, daß die Ausführung sehr langsam ist. Alle anderen Hochsprachen benötigen einen echten Bildschirmeditor. Dieser kann vom Funktionsumfang sehr primitiv sein (also nichts mit Block markieren und Copy&Paste), schließlich war BASIC auch nicht komfortabler, aber so ein paar Kleinigkeiten (automatischer Einzug der Folgezeile bei Return) wären sehr leicht einzubauen. Nett wäre es, wenn der Editor einen virtuellen 80-Zeichen-Bildschirm zur Verfügung stellen könnte, von dem man stets einen 40-Zeichen-Ausschnitt sieht (vgl. UCSD-Pascal). Der Editor sollte zwei Sprungvektoren zur Verfügung stellen, einen zum Kodieren, einen zum Dekodieren einer Zeile. Diese werden aufgerufen wenn eine neue Zeile angezeigt oder eine editierte Zeile verlassen und der Inhalt im Speicher übernommen werden soll. Vorteil: Man könnte bei einer Programmiersprache die Zeile automatisch tokenisieren und teilweise überprüfen. Damit würde sich solch ein Editor auch zur Eingabe von Assemblercode eignen. Deaktiviert man die Kodierung und Dekodierung, kann man auch einfachen Text eingeben.
    Wenn nun der Quelltext nicht mehr auf Zeilen mit Zeilennummern basiert, müßte man sich für ein BASIC überlegen, wie man Sprünge realisieren will. Eine Möglichkeit wäre, anstelle von Zeilennummern Labels zu verwenden. Hier könnte gelten: Beginnt eine Basiczeile mit einem Leerzeichen => kein Label. Ansonsten: Label. (Weitere Ausnahmen könnten noch Kommentare sein.)

    Code
    1. A=1
    2. l A=A+1
    3. IF A<>10 THEN l

    Der Interpreter würde bei einem Sprung nun nicht mehr nach Zeilennummern suchen, sondern nach den Labels. Um diesen Vorgang zu beschleunigen, wäre es vielleicht möglich, bei Programmstart den Programmtext zu scannen und die Zeilen in einer Liste zu sammeln, in denen Labels stehen. Der Suchvorgang würde sich dann nur auf diese Zeilen beziehen. Damit sollte eine ähnliche Geschwindigkeit wie bei V2-Basic erreicht werden können. Auf jeden Fall sähe solch ein BASIC sauberer aus (eher wie ein modernes BASIC), als wenn vorne dauernd die Zeilennummern stehen, die man ja doch nicht dauernd als Zieladressen braucht. Der Rest des BASICs könnte zunächst gleich bleiben, aber natürlich auch um einen Befehl zum Zeichnen eines Grafikpunktes ergänzt werden. ^^
    Wählt man einen Bildschirmeditor für die Eingabe des Quelltextes, so ergibt sich daraus jedoch auch, daß es keinen interaktiven Direktmodus mehr gibt. Man kann also nicht einfach SAVE "PROGRAMM", 8 eingeben, um den Text zu speichern. Das müßten Befehle im Editor erledigen, z. B. CTRL-L = Datei laden, CTRL-S = Datei speichern. Auch ein PRINT 3+3 geht dann nicht mehr, geschweige denn LOAD"$",8:LIST. Mit anderen Worten: Man benötigt auch ein anderes System, um mit dem Computer an sich zu kommunizieren. Und hier wird die Sache spannend.


    Zitat

    wobei sich fast alles auf dem Daten-Stack abspielt. Dabei liegen Daten- und Return-Stack irgenwo im RAM (nicht der System-Stack!),

    Bei Stackmaschinen unterteilt man aus Gründen der Effizienz den Stack in zwei getrennte Stacks: Datenstack (Variablen, Rücksprungsadresse (Stackframe)) und Evaluationsstack. Direkte Operationen auf einem Datenstack, dessen TOP irgendwo im Speicher liegt, sind viel zu langsam, da sie mit "LDA (ind), y" umgesetzt werden müßten. Dahingegen wird z. B. bei UCSD-Pascal der 6502-Stack als Evaluationsstack verwendet. Ein Laden eines Wertes entspricht damit dem Pushen des Wertes (PHA) auf den Stack. Insgesamt ergibt sich bei geschickter Programmierung eine brauchbare Geschwindigkeit. Nachteil: Bei 16-Bit-Stackworten hat der Stack eine Maximaltiefe von 128 Worten. Das bedeutet, daß es bei einer rekursiven Verschachtelung von Funktionen der Form

    Code
    1. z := a + funktion;

    zu einem Überlauf kommen kann, da bei jeder Abarbeitung zunächst a auf den Stack gebracht wird und dort verbleibt, bis "funktion" beendet worden ist. Hierdurch kann UCSD-Pascal zum Absturz gebracht werden.
    Alternativ kann man den Evaluationsstack im Speicher (auch auf der Zeropage) anlegen und mit "abs, x" oder "zp, x" adressieren, was teilweise um ein paar Taktzyklen schneller ist. Hierbei reicht es in der Regel aus, wenn dieser Stack nur eine Tiefe von 12-16 hat und das auch nur, wenn man ihn für die Parameterübergabe gebraucht. Zwingende Voraussetzung wäre in diesem Fall dann, daß vor dem Aufruf einer Funktion ein Flush durchgeführt wird, d. h. das Sichern des Stackinhalts auf den Datenstack mit anschließender Wiederherstellung. Dies ist allerdings nur notwendig, wenn vor der Funktion ein nichtkonstanter Wert geladen wurde. Vorteil dadurch: Kein Absturz mehr.

    Zweiadreß-Instruktionen oder Dreiadreß-Instruktionen

    Derartige Instruktionen machen auf einer Stackmaschine nicht wirklich Sinn. Das Ziel besteht ja gerade darin, stets auf den ersten oberen Elementen zu operiern, um dadurch den Bytecode kurz und die Ausführung schnell zu halten. Eine Dreiadreß-Kodierung dürfte damit kaum infrage kommen, denn bei einem in Silikon gegossenen Prozessor oder einem FPGA kann die Adreßdekodierung zwar parallel ausgeführt werden, bei einem Interpreter/Emulator jedoch nur sequentiell. Um einen einigermaßen schnellen Interpreter hinzubekommen, sollten die Befehle so aufgebaut sein, daß der eigentliche Befehlsanteil einem Byte entspricht und eventuelle Parameter den folgenden Bytes, damit mittels Sprungliste und "JMP (ind)" die passende Instruktionroutine schnell aufgerufen werden kann. Jede weitere Dekodierung außerhalb dieser Sprungtabelle verlangsamt die Ausführung drastisch, insbesondere wenn es sich um Bitfelder innerhalb des Befehls handelt, die ausmaskiert und ausgewertet werden sollen, was auch oftmals mit einem zusätzlichen Shiften verbunden ist.
    Viele Compilerbauer stehen auf 3-Adreßkkodierung, weil es ihren Algorithmen so schön nahe kommt. In der Praxis zeigt sich jedoch, daß eine solche Kodierung nur in sehr seltenen Fällen zur Anwendung kommt, denn der Haupteil der Operationen arbeitet nach dem Prinzip, daß die Einzelbestandteile der Operationen (Faktor usw.) nach Ausführung nicht mehr verwendet werden. Ausnahmen bilden wohl Variablen, die man in den Registern als Minicache hinterlegt, aber auch dann gilt, daß sich ein Dreiadreßbefehl nur wirklich lohnt, wenn der Befehl kürzer und schneller ist als sein 2-Adreß-Pendant. Wirft man einen Blick auf den ARM und den 68000, so erkennt man:
    ARM: Rx := Ry + Rz = 32 Bits
    68000: 1.) Rx := Ry
    2.) Rx := Rx + Rz = 32 Bits
    Der 68000-Code ist genauso lang, dafür aber in den häufigen Fällen, in denen man lediglich eine 2-Adreßkodierung benötigt, kürzer. Die Geschwindigkeit muß sich auch nicht unterscheiden, sofern man im Prozessor sowas einbaut wie Befehlszusammenfassung, d. h. der Prozessor bindet kleine Befehlsgruppen der oben genannten Form von sich aus zusammen.
    Fazit: Dreiadreß-Kodierung wird häufig über- und Zweiadreß-Kodierung unterschätzt.
    Beide erzeugen aber im Vergleich zu einer Stackmaschine längeren und damit in der Interpreterausführung langsameren Code. Um die Register einer Registermaschine effzient zu belegen, braucht es aufwendige Berechnungen durch den Compiler. Eine Stackmaschine kümmert sich um sowas (so gut wie) gar nicht. Wenn es also darum geht, eventuell einen Compiler direkt auf dem C64 laufen zu lassen, sollte man von allen Registermaschinen-artigen Befehlen Abstand nehmen.

    Registerfenster

    Vor einiger Zeit machte ich eine Untersuchung bezüglich der Effizienz von Registerfenstern. Was ursprünglich wie eine gute Idee aussah, entpuppte sich in der Praxis als eher umständlich und bei der Ausführung auf einem Emulator als langsam. Persönlich kann ich von dem Konzept des Registerfensters nur abraten. Wenn ich mich recht erinnere gibt es hierzu auch eine offzielle Studie (Harvard?), die zu einem ähnlichen Ergebnis kommt.


    Die folgenden Anmerkungen entsprechen nur meiner persönlichen Vorstellung einer brauchbaren Hochsprache für den C64 und sind logischerweise alles andere als maßgebend.

    - wieviel Arithmetik brauche ich? wieviel "höhere Mathematik"? transzendente Funktionen? 3D ? Komplexe Zahlen? Welche Genauigkeit?

    Genauigkeit:
    Arithmetik: Standardrechenoperationen + - * DIV bzw. / << >> (vielleicht auch >>>), Bitoperationen &, |, ^ und ~ sowie boolsche Operationen AND, OR, XOR und NOT.
    Für Zeichenketten sollte ferner eine einfache Stringkonkatenation über '+' möglich sein.
    Zeichen: 8 Bit, Integer: 16 Bit, Fließkomma, 32 Bit. Komplexe Zahlen: - (Auf dem C64? Wofür?)
    Zusätzliche Funktionen können über Bibliotheken eingebunden werden und müssen nicht zwingend Bestandteil der Sprache sein.

    - welche Datentypen, wenn überhaupt? Genügen "Variant"-Typen die sich ihrem Inhalt anpassen

    Hier würde ich die üblichen Verdächtigen verhaften: Boolean, Zeichen, Ganzzahl, Fließkommazahl und Zeichenkette, egal wie man sie in der Sprache nennt. Eventuell um besonders bei Feldern Platz zu sparen noch sowas wie Byte (Zahl zwischen 0..255). Eine starke Typisierung bringt den Vorteil, daß zur Laufzeit nicht andauernd getestet werden muß, was konkret vorliegt. Der erzeugte Code ist dadurch wesentlich schneller.
    Desweiteren wären sinnvoll Verbundstrukturen (Struct bzw. Record und Union) und mehrdimensionale Felder. In den meisten Fällen reicht es aus, wenn Felder eine feste Größe haben.
    Prozedurvariablen wären auch ganz nett, da sich damit wie in C sowas wie Objekte und Methoden basteln lassen.

    Kontrollstrukturen, Blöcke, Flußsteuerung (wenn nicht gerade "Funktionale Sprache!") FOR .. NEXT Entsprechungen; REPEAT ..

    Auch hier würde ich mich an dem Standard orientieren: abweisende und annehmende Schleife, Zählschleife, Endlosschleife. Bei Verzweigungen IF, ELSE, vielleicht auch ELSIF. Eine Switch- bzw. CASE-anweisung erleichtert Schreibarbeit. Sinnvoll für Schleifen: break, continue. Im Gegensatz zu dem, was Wirth sagt, kann ein Goto manchmal ganz nützlich sein.

    I/O Funktionen Konsole, Bildschirm, Massenspeicher-Einbindung

    Nicht Elemente der Sprache an sich. Wird über Bibliotheken zur Verfügung gestellt, die in der Sprache selbst oder in Assembler geschrieben sein können. Für solche Sachen kann es keine absolut gültigen Befehle in der Sprache geben, da die einzelnen Routinen zu sehr systemabhängig sind.

    Schnittstelle zur Memory-Verwaltung (Reservierter Speicher, Heap-Verwaltung..)

    Am besten gar nicht. Der Speicher des C64 ist zu klein, als daß sich eine Speicherverwaltung lohnen würde. Außerdem sollten dem Programmierer keine Restriktionen auferlegt werden bezüglich des Ansprechens des Speichers. Homecomputer sind dafür da, daß man als Programmierer in ihnen wild herumpoken kann, solange man weiß, was man tut. Treibt man es zu bunt ==> Absturz, Pech gehabt. Dieses Verhalten sollte sich von V2 Basic nicht unterscheiden.

    Hierarchien , Units, Includes

    Es sollte die Möglichkeit bestehen, verschiedene auch geschachtelte Namensräume zu definieren. Die einfachste Möglichkeit des Includes ist das Einbinden eines zusätzlichen Quelltextes. Nachteil: Die Bibliothek muß jedes Mal neu übersetzt werden. Vorteil: Nicht gebrauchte Routinen werden rausgeschmissen und belegen nicht unnötig kostbaren Speicher. Das Programm wird am Ende kürzer und eventuell schneller. Natürlich kann man sich ein kompliziertes Objektcodeformat ausdenken und anschließend an den Kompilierungsvorgang alle Bestandteile miteinander verlinken. Kann man alles machen. Fragt sich nur, ob das am Ende a) einfacher, b) schneller wäre.

    Mit welchen Konzepten kommt man mit wenig Aufwand am weitesten

    Meiner Erfahrung nach kommt man mit dem Sprachumfang von Pascal bzw. besser noch den Nachfolgern Modula2 und Oberon (abgesehen von den Modulen) gut zurecht, da sie alles bieten, was man so für die Programmierung am C64 braucht. Diese Sprachen (bzw. eine BASIC-Variante davon, sieht sowieso alles irgendwie gleich aus) eignen sich sowohl für "ernsthafte" Programme als auch diverse Spiele. C nervt mich persönlich mit Sachen wie vorzeichenbehaftete Zeichen, keine richtigen Namensräume, umständliche Syntax (bla->element), und Java, C# und andere objektorientierte Sprachen dürften zu groß sein für eine brauchbare Umsetzung auf einer 64kb-Maschine. Persönlich würde ich eine Sprache im Stil von BASIC oder Oberon bevorzugen mit ein paar leicht zu erlernenden und handbaren Kontrollstrukturen und der Möglichkeit, Unterroutinen in Assembler einbinden zu können.

  • Auch bei Turbo-Pascal gab es die Möglichkeit, in die Shell zurückzuwechseln , auch temporär. (F10 = Menü).


    Die Möglichkeit könnte auch das neu zu schaffende System bieten.
    Der Bildschirmeditor wäre wie bisher ein Full-Screen-Editor.


    Es gäbe verdeckte Zeilennummern, von denen im Normalfall, wenn man im Programm-Editiermodus ist, nur die letzte Stelle als linke Spalte eingeblendet ist. an dieser Pseudo-oder Kurzzeilennummer kann das System ja unterscheiden, in welchem Modus gerade gewerkelt wird. Und auch der Benutzer kann sofort erkennen, in welchem Modus er sich befindet.


    Diese letzte Ziffer könnte man auch als Ad-hoc-Kurz-Label benutzen für kleine lokale Schleifen. (mit vorgesetztem Sonderzeichen zur Unterscheidung vor / rückwärtssprung)


    Beim Direktmodus befände man sich in einer vom System bereitgestellten Laufzeitumgebung die bei Pascal "PROGRAM DIrectmode {...} BEGIN ... END; " entspräche. Schön wäre, wenn man aus diesem Sandkasten heraus Module / Prozeduren aus dem Speicher testen könnte.


    Umschaltung z.b. durch SYS-Befehl oder Funktionstaste auf Editor-Modus. Wenn der C64 schon Funktionstasten hat ..


    Die Idee eines Visual-Basic - Oberon -Verschnitts mit der Benutzeroberfläche von Turbo Vision (Klötzchengrafik zur Rechenzeit-Schonung aber zur Not auch mausbedienbar) und einem 8-bit-freundlichen Kellerautomaten unter der Haube finde ich elektrisierend... hoffentlich kann man das schön auf das wesentliche begrenzt halten - ROMable - und dennoch eine spannende Bibliothek in den Raum der Möglichkeiten bringen.

  • Danke für die Antwort, aber leider muß ich gestehen, daß ich noch nicht alles verstanden habe. :(

    Der Bildschirmeditor wäre wie bisher ein Full-Screen-Editor.

    Nur für den Fall, daß Du mit bisher meinst, daß der Basic-Zeilen-Editor ein Full-Screen-Editor war, so würde ich sagen, nein, denn die Anzeige auf dem Bildschirm entsprach nicht der Reihenfolge der Zeilen im Text. Die Zeilennummer bestimmte, an welcher Stelle die Zeile im Text eingefügt wurde. Bei einem Full-Screen-Editor ist es allein die aktuelle Cursorposition.

    Auch bei Turbo-Pascal gab es die Möglichkeit, in die Shell zurückzuwechseln

    Der Witz ist ja, daß man eine solche Shell überhaupt nicht mehr benötigt. Das, was man noch an Verwaltungsaufgaben erledigen muß, läßt sich auch in einem Dateibrowser o. ä. vornehmen. Es gibt keinen Grund, warum man noch eine Kommandozeile zur Befehlseingabe verwenden sollte, in der man dann Befehle tippen muß wie "DIR" usw., wenn man den gleichen Effekt durch ein Menü und einfachem Tastendruck erzielt. Möchte man mehrere Befehle miteinander verbinden, würde es sich eh anbieten, diese als MIniprogramm zu schreiben.

    hoffentlich kann man das schön auf das wesentliche begrenzt halten - ROMable - und dennoch eine spannende Bibliothek in den Raum der Möglichkeiten bringen.

    Was den Editor anbelangt, so sehe ich da kein Problem. Einen primitiven Editor kann man sicherlich ins Roms quetschen. Sorgen macht mir der Compiler. Selbst ein nichtoptimierender Compiler wie der von Florian Matthes würde schon 20kb schlucken. Da wäre der Bytecodeinterpreter mit ca. 3kb noch das kleinste Übel. Auch die umfangreichen Tabellen für die Bezeichner, die der Compiler während des Compilevorgangs anlegt, könnten bei 64kb verhindern, daß Quelltext und Compiler gleichzeitig im Speicher liegen. Das bedeutet, daß wie bei UCSD-Pascal zunächst der Quelltext gespeichert und der Editor verlassen werden muß. Danach ruft man den Compiler auf, der den Text sukzessive von Disk lädt und dabei ebenso den Code sukzessive auf Diskette ablegt. Das ist nicht nur umständlicher als in TurboPascal, sondern auch erheblich langsamer. (Ich habe die langen Wartezeiten von damals[tm] noch gut in Erinnerung.)
    Schwierig wird es auch bei den Bibliotheken. Da wüßte ich nicht, wo man die anders ablegen sollte als auf einem Massenspeicher wie der Diskette. Wie ich früher schon schrieb, hielte ich es für sinnvoll, wenn der erzeugte Code mittels Spezialbefehl in eine Assemblerroutine springen könnte, die sich ihre Parameter vom Evaluationsstack holt. Hier könnte man einerseits Systemroutinen einbinden zur Dateibehandlung als auch Grafikbefehle, Soundbefehle oder was immer das Herz begehrt. Allerdings dürften diese nicht alle als default in den Speicher passen, sondern müßten als Module zunächst geladen werden. Dazu bräuchte man ein Dateiformat, welches angibt, mit welchen Adressen die Sprungliste der externen Befehle gepatcht werden kann. Leider ist es auf dem 6502 nicht ohne Weiteres möglich, relokatiblen Code zu schreiben. Da könnte es zu Problemen kommen, wenn Bibliotheken sich in ihren Speicherbereichen überschneiden. (UCSD-Pascal verfügt tatsächlich über ein Verfahren, absolute Adressen im 6502-Code nach dem Laden zu patchen, so daß der Code relokatibel wird.)
    Bevor man also überlegt, solch ein System ins Rom zu gießen, sollte man vorher vielleicht einfach mal austesten, ob sich solch ein System ähnlich wie UCSD-Pascal von Diskette starten lassen kann, d. h. Compiler, Eduitor etc befinden sich auf Diskette und werden bei Bedarf geladen. Wenn das dann funktioniert, kann man anfangen, über eine Romimplementierung nachzudenken.

  • Funktionen und Kontroll-Strukturen

    Blöcke sind Strukturen, die Anweisungen in Ausdrücke einschließen können. Grundsätzlich bildet man Blöcke duch eckige Klammern (vergleichbar den Geschweiften in C und ähnlichen Sprachen):


    [ Anweisung; Anweisung; .. ]


    ist ein Ausdruck, der dementsprechend einen Wert auf dem Stack hinterlässt. Will man eine Anweisung daraus machen, braucht es einen Strichpunkt dahinter. Will man einen ganz bestimmten Wert hinterlassen, nutzt man dazu das Schlüssenwort "return", wie in anderen Sprachen auch, nur dass es eben auch für Blöcke, und nicht nur für Funktionen Anwendung findet.


    Grundsätzlich werden alle lokalen Variablen, die in Blöcken definiert werden, ausserhalb wieder "vergessen".


    Beispiel:


    [ return 3; ]


    würde als Ausdruck den Wert 3 ergeben. Somit hätte der Gesamtausdruck


    + 5 [ return 3; ]


    den Wert 8.


    Ausdrücke haben eine weitere Eigenschaft: Sie können erfolgreich sein, oder schief gehen. In dem Fall bricht das Programm mit einer Ausnahme ab. Beispiel:


    X >= 5;


    verhält sich wie eine "Assertion", also bricht der Block ab, falls X kleiner 5 ist.


    Funktionen sind zuerst einmal ganz normale Typen, und können ca. so geschrieben werden:


    ArgumentAusdruck -> Resultatsausdruck


    Dabei können die Argumente reine Variablen, oder auch Tupel mehrerer Variablen oder Ausdrücke sein. Es ist möglich, mehrere Funktionausdrücke zu kombinieren:


    (Arg0 -> Res0) & (Arg1 -> Res1) & ..


    Man kann solche Funktionsausdrücke Variablen zuweisen, dann hat man eine benannte Funktion. Beispiel:


    fac := 0 -> 1
    & 1 -> 1
    & (int n) -> * n fac (- n 1);


    (in dem Fall würde ich übrigens Stephan zustimmen, dass man das besser iterativ löst. Es ist hier auch nur als Beispiel für Funktionsausdrücke so dargestellt.)


    Das läuft so ab:


    fac 3 .. ( 0 = 3 -> 1 ) .. geht schief weil 0 nicht 3 ist
    .. ( 1 = 3 -> 1 ) .. geht schief weil 1 nicht 3 ist
    .. ( int n = 3 -> * n fac ( - n 1 ) ).. funktioniert weil n = 3 gesetzt werden kann
    .. * 3 fac ( - 3 1 )
    ..* 3 fac 2
    .. * 3 ( 0 = 2 -> 1 ) .. geht schief weil 0 nicht 2 ist


    .. * 3 ( 1 = 2 -> 1 ) .. geht schief weil 1 nicht 2 ist


    .. * 3 ( n = 2 -> * n fac ( - n 1 ) ) .. funktioniert weil n = 2 gesetzt werden kann
    ..* 3 * 2 fac ( - 2 1 ) )
    ..* 3 * 2 fac 1
    ..* 3 * 2 ( 0 = 1 -> 1 ) .. geht schief weil 0 nicht 1 ist
    ..* 3 * 2 ( 1 = 1 -> 1 ) .. funktioniert weil 1 1 ist
    ..* 3 * 2 1


    fac 3 => 6


    Funktionsausdrücke können so genutzt werden wie "case"-Statements in anderen Programmiersprachen. Beispiel:


    ( 1 -> ersterAusdruck ) &

    ( 2 -> zweiterAusdruck ) &

    ( n -> dritterAusdruck ) x


    wäre in C ca. sowas:


    Funktionsausdrücke können nicht nur mit Argumenten konstruiert werden, sondern auch mit dem Zustand eines Blocks. So beschreibt z.B.:


    [ = x 1; ] -> 3


    Wo ursprünglich die Gleichsetzung (nicht mit Zuweisung verwechseln!) einer Variable mit einem Argument steht, steht jetzt ein Block. Beide (Gleichsetzung und Block) können Ausnahmen hervorrufen (also schief gehen), und daher können beide in so einem funktionalen Ausdruck vorkommen.


    Man kann man damit ein if then else darstellen:


    ( Bedingung -> If-Ausdruck ) & ( [] -> Else-Ausdruck )


    wäre sinngemäß dasselbe wie



    Code
    1. if(Bedingung)
    2. If-Ausdruck;
    3. else
    4. Else-Ausdruck;

    während ( Bedingung -> If-Ausdruck ) auch ohne "else"-Zweig möglich ist. Zuguterletzt kann man das Argument (oder den default-Wert) weglassen, wenn es nicht wichtig ist. Siehe


    ( Bedingung -> If-Ausdruck ) & ( -> Else-Ausdruck )


    Schleifen werden durch zwei Anweisungen ermöglicht: repeat und return, die innerhalb von Blöcken sich immer auf den innersten Block beziehen.


    Beispiel:


    x := 1;
    e := n;
    [
    [ = e 0 ] -> return;
    x := * x n;
    e := - e 1;
    repeat
    ]


    würde x = n hoch e berechnen.



    Im Prinzip kann man damit bereits alle gängigen Kontroll-Strukturen erschlagen, aber man kann sich das syntaktisch erleichtern, und eigene Strukturen basteln. Darauf gehe ich noch ein.

  • Wieso die Festlegung auf 16kb ROM? Ein 64er sollte doch auch 24/32kb hingekommen, wie Atari XL oder CPC.

    Soviel ROM-Speicher wie auf dem CPC wäre sicher wünschenswert. Die Vorgabe ist hier aber leider, daß das Rom nicht größer sein darf als das alte BASIC-Rom (das Kernal-Rom wird zunächst als unverändert vorausgesetzt mit seinen Routinen zum Ansprechen des seriellen Bus usw.) In 32kb kann man wahrlich eine ganze Menge reinpacken. Editor und Assembler benötigen zusammen weniger als 16kb. Und ein Compiler könnte dann den Rest gebrauchen. Nur leider, leider...