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?

  • Weil nur so der Fortschritt neuzeitlicher Sprachen am sichtbarsten ist.
    Ein direkter und aussagekräftiger Vergleich mit dem Original-C64 und Original-ROM-Größe ist möglich.


    ROM-Doping wäre unsportlich und gegen den olympischen Gedanken.
    (Aber ein gleichartiges Projekt für einen CPC 6128 - mit dessen ROM-Grösse - wäre natürlich möglich und sollte dann zu Vergleichen zwischen diesem Rechner und seiner Neuinterpretation führen.)

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

    Die Draufsicht: Die Stacks sind genau die Stacks der virtuellen Maschine. Ich habe übrigens grad M.J.s Ausführungen durchgelesen, und die Argumente, warum Register-Fenster nicht unbedingt ratsam sind, sind einleuchtend. Daher kann es sein, dass meine ursprüngliche Idee, die Stacks in der ZP zu spiegeln, eher überdenkenswert sind. Habe ich dankend aufgenommen. Ebenso die (offenbar aus der Praxis kommende) Anregung, Daten- und Evaluationsstack zu teilen, wobei dann die Variablen (was ich auch nicht so vorgehabt habe) mit den Rücksprung-Addressen zusammen untergebracht wären.


    Diese Kommentare haben mir bereits einiges zu denken gegeben.


    Was ich allerdings dabei nicht verstanden habe, ist, wie eine VM es schafft, mit Daten auf dem Stack zu arbeiten, während da ständig Rücksprungaddressen gepusht und gepoppt werden. Ich meine das hier:


    Dahingegen wird z. B. bei UCSD-Pascal der 6502-Stack als Evaluationsstack verwendet.

    Obwohl mir gerade schwant, dass eine geschickte Implementierung vielleicht gar nur eine Schleife mit geschickt gemachter Sprungtabelle benötigt, und nach Abarbeitung eines Befehls ohnehin nur wieder an den Anfang der Schleife gesprungen werden muss. Ist das der Grund, dass das geht, H.J.?


    Grundsätzlich aber ist es mir darum gegangen, den HW-Stack möglicht in Ruhe zu lassen und der VM zu überlassen, während sich die virtuelle Maschine ihre Stacks im Speicher definiert.


    Meine Sprachdefinition hat sich daher darauf bezogen, was die einzelnen Sprachkonstrukte auf dem VM-Stack so bewirken. Da ich allerdings im Augenblick von schwammig nach unscharf unterwegs bin, ist es leicht möglich, dass ich das (und einiges mehr) nicht ausreichend klargestellt habe.

  • Möglich wäre sowas doch mit so einer Erweiterung:


    [Externes Medium: https://youtu.be/5WjE42YjBy4]


    Hier der dazugehörige Thread bei Lemon64:
    http://www.lemon64.com/forum/viewtopic.php?p=645708#645708


    Interessant an dem Projekt ist, dass es sich um eine 1MB RAM Erweiterung handelt, in der man zwischen 16 Bänken umschalten kann. Und soweit ich das verstanden habe, kann man aus einer Bank in eine andere Bank schreiben.
    Damit könnte man z.B. In einer Bank das Entwicklungssystem laufen lassen und in eine andere Bank compilieren.
    Dafür wurde wohl Turbo Assembler 5 angepasst.
    http://www.lemon64.com/forum/viewtopic.php?p=608252#608252

  • Kannst Du bitte den richtigen Link zu Lemon64 posten, ohne dass ich mich mit Deinen User-Daten auf Lemon64 einloggen muss? ;-)


    Ich schätze deine Beiträge sehr, trotzdem eine (weitere) Bitte: Kannst du bitte mit eigenen Worten (=ASCII-Text) benennen, worum , um welches Cartridge es geht?


    (Ich gebe ungern meine Daten und Präferenzen an Google/youtube indem ich wahllos auf ein Video klicke)


    by the way, wenn Du sagst "das gibt es doch schon" könnte man das ja so auffassen: Hier kann man sich abgucken, wie man so etwas konkret implementiert (und weniger als: alle Aktivitäten in dieser Richtung sind sinnlos weil es sowas doch schon gibt.)

  • 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.

    Kritik vorbehaltlos akzeptiert. Es ist so! Ich habe hier quasi das Experiment gemacht, laut zu denken, aber oft schreibe ich nicht dazu, wohin ich damit überhaupt will. Das kann irritierend und nervig sein, wenn man in meinen Kopf nicht hineinschaut.


    Mich erzieht hoffentlich diese Kritik dazu, in Zukunft genau darüber nachzudenken, und es vorher zu Ende zu denken, und die wichtigen Ziele gleich zu formulieren. Das hat dann darüber hinaus den Vorteil, dass ich mich nicht so leicht in Wunschvorstellungen versteige, von denen mir selbst noch gar nicht klar ist, ob sie überhaupt in sinnvoller Weise machbar sind.

    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.

    Ich halte das zwar für eine ganz faszinierende Idee (die so neu natürlich nicht ist), aber wenn sie, wie du sagst, die 6502 tatsächlich zu sehr piesakt (wahrscheinlich würde sie eher die VM in ihrer Konzeption piesaken), dann ist sie mit Vorsicht zu genießen, und das gilts erst einmal durchzudenken.


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

    Was dagegen spricht, ist folgendes: Ich gehe von einer Laufzeit-Umgebung aus, in der ich Funktionen (und noch ein paar nette Dinge) dynamisch hinschreibe und benutze. Also wird es auf jeden Fall auch schon während der interaktiven Entwicklung Type-Checks geben müssen, sonst müsste ich ein ganzes Programm statisch checken, und das bedeutet, ich muss es in einem Ruck durch compilieren.


    Dieser Ansatz ist grundsätzlich gut und wird von manchen ganz klar bevorzugt. Ich halte den auch für sehr vernünftig. Nur kann ich ihn auf der Basis hier nicht adoptieren, denn dann bräuchte ich tatsächlich einen Editor für den ganzen Quelltext, einen Compiler (1-pass oder 2-pass), der alles immer insgesamt übersetzt, und dann bin ich bereits in Teufels Küche, wenn ich damit einfach nur "loslegen und Spaß haben" will, wie das in Basic möglich ist.


    Darum bin ich davon ausgegangen, dass ich - wie halt auch in BASIC - in kleinen Einheiten iterativ implementiere - teste - ändere - teste - usw.


    Wenn ich davon ausgehe, brauche ich aber sowieso solche "Matching"-Funktionalität, die mir für bestimmte Stack-Segmente entscheidet, ob sie zu einer bestimmten Typ-Definition passen (z.B. zu Parameter-Listen).


    Wenn ich das aber ohnehin brauche, dann wäre ich ja verrückt, wenn ich so einen Mechanismus nicht voll ausnutze. Dann habe ich dynamisches Typen bereits durch die Hintertür erhalten, wieso dann nicht auch die Nutzung entsprechender Sprach-Elemente durch die Vordertür, die ich dann sowieso "frei Haus" erhalte?


    Die Sache mit den Tuples ist vielleicht syntaktisch nicht ideal, und nicht mit idealen Beispielen gebracht. Natürlich, das, was ich da als Beispiele gebracht habe, ließe sich mit anderen Mitteln vielleicht effizienter bewerkstelligen. Was ich wirklich wollte, war, die Funktionsweise damit zu demonstrieren, aber die gegebenen Beispiele damit nicht als Hauptzweck darstellen.


    Der Hauptzweck wäre wie gesagt ein "Matching", das es mir erlaubt, zu testen, ob ein Tupel (also ein Stack-Ausschnitt mit gegebener Variabel-Strukturierung) einer Definition ungefähr entspricht, und damit später ev. die syntaktisch einfache Erzeugung von dynamisch definierten "Objekten" (also deren Definition zur Laufzeit vorliegt).


    Funktionen als Werte erster Klasse sind mir ebenfalls ein Anliegen, allerdings ist das natürlich noch eine diffizilere Geschichte. Auch sowas will wirklich gut überlegt sein, und zahlt sich wirklich nur aus, wenn sich dafür einfache und schnelle Mechanismen finden lassen.


    Wenn es jetzt dem einen oder anderen von euch - mit ganz anderen Vorstellungen im Kopf - die Haare aufstellt :schreck!: , habe ich volles Verständnis! :-)


    Was mir bleibt, ist auf jeden Fall, mir zu überlegen, ob ich mit manchen Konzepten nicht bereits jenseits des Machbaren bin.

  • Obwohl mir gerade schwant, dass eine geschickte Implementierung vielleicht gar nur eine Schleife mit geschickt gemachter Sprungtabelle benötigt, und nach Abarbeitung eines Befehls ohnehin nur wieder an den Anfang der Schleife gesprungen werden muss. Ist das der Grund, dass das geht

    Ja, genau das ist der Grund. Diese Schleife muß extrem kurz sein, damit die Endgeschwindigkeit brauchbar bleibt. Aufrufe von Unterprogrammen verbieten sich aus Geschwindigkeitsgründen und sollten nur in Ausnahmefällen zur Anwendung kommen.


    Was die Ausführungen zum Sprachkonzept anbelangt, so muß ich leider sagen, daß ich den Erklärungen zu Tupeln und Block etc nicht so ganz folgen konnte. Daher meine Bitte: Wäre es möglich, folgenden kleinen Bubblesort-Algorithmus (ungetestet) aus meinem alten Lehrbuch in Deine Sprache zu übersetzen, damit man mal besser nachvollziehen kann, wie sowas in der Praxis aussieht:

    Vielen Dank im voraus!


    Edit: Die Umsetzung muß natürlich nicht perfekt sein. Eine grobe Formulierung könnte eventuell schon reichen, um sich mal ein Bild zu machen.

  • Ich hab jetzt schon verstanden, dass das Ganze erstmal ein Brainstorm zu möglichen Anforderungen und deren Umsetzung sein soll. Ich bin mir trotzdem nicht sicher, ob ich so recht folgen kann.


    Das mag auch daher kommen, dass ich herzlich wenig Kontakt zu Stack-orientierten Sachen gehabt habe.
    Stack zur Parameter-Übergabe an Unterprogramme ist sehr sehr schön, ich sehe, dass man bei tieferen Unterprogramm-Aufrufen oder Rekursionen nicht das Theater von Assembler mit zu wenig Registern oder das von V2 mit globalen Variablen als Parameter hat.


    UPN zu schreiben geht von mir aus ebenfalls (ich würde aber die Schulformel-Schreibweise von Basic definitiv vorziehen).


    Ich kann mir auch vorstellen, dass man Zugriffe relativ zu einem SP "umsonst" in Hardware haben kann, aber als VM wird das was kosten.
    Irgendwie scheint mir schon mit meinem halbem Verständnis der Stack keine Allzweckwaffe zur Speicherung von allem zu sein.
    "Compiliert" sicher prima mit relativer Adressierung auf den Stack, aber als Ersatz für das beschränkte V2?


    An dieser Stelle frage ich mich, für welchen Einsatz die VM gemacht wird.
    1) "compiliert" wie Sweet16, um ROM-Speicher in unkritischen Routinen zu sparen?
    2) "uncompiliert" im Rahmen des Basic-Ersatzes?
    3) Beides gemeinsam?
    4) Oder doch eher gedanklich frei und sehen, welche Konzepte prima zur VM passen und umgekehrt, ob die VM schöne Konzepte ermöglicht?


    1) Für den Zweck darf es kurz sein, ohne all die schönen Dinge wie Variablen-Namen, Laufzeit-Checks, lokale Variablen, Name Spaces, Rekursionen usw.
    Es darf durchaus Cross am PC geschrieben und dort gecheckt werden.
    Die Anwendungen im ROM sind sehr übersichtlich, ich sehe den Editor, Programm tokenisieren, List, evtl. Screenausgaben, Tastatureingaben... Alles recht schlicht, wo sehr viel über die simpelsten Typen Byte und Word gemacht werden kann.
    Soweit sehe ich also Kommandos vor meinem inneren Auge, die mit "absoluten" Adressen (bzw. einer Base-Adresse) arbeiten und möglichst simpel den Datentyp erkennen oder ihn gar im Befehlswort enthalten.


    2) Bill Gates war wirklich nicht so übel, der Schritt vom sehr beschränkten V2 zu einer Sprache mit Übergabeparametern, Name-Spaces, Übergaben von Funktionsadressen, lokalen Variablen, Label... der scheint mir doch etwas groß, auch wenn man noch viel Speicher im CBM-Teil des ROMs plündern kann. Wegen der Beschränkungen des Systems denke ich auch, dass sich dieser Basic-Ersatz schon an der VM orientieren müsste. Wenn z.B. die VM UPN nutzt, dann kommt bestimmt nicht noch die Formelauswertung a la Basic hinzu.


    3) beides gemeinsam? Vielleicht gar parallel, um z.B. Tastatureingaben im IRQ per "Basic" abzufackeln? Wenn sich die Variablen nicht in die Quere kommen... Ein Konzept dazu wird sicher auch noch für Multitasking nützlich.

    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?

    Identifier sind Variablennamen, Funktions/Labelnamen und so?
    Freie Formatierung find ich nicht zwingend, genug Assembler kommen auch ohne aus. Einrücken könnte Sache von LIST werden, falls der Platz bleibt.

    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.

    Ich denke, dass sich die Sprache der VM und die Anwendersprache sehr ähnlich sein dürften. Ein "Basic"-Befehl wird auch 1 Befehl der VM sein. Verschiedene Varianten für verschiedene Parameter kann ich mir aber gut vorstellen, falls genug ROM da ist. Jedenfalls sollten weder der Parser noch der LIST-Befehl X einzelne Befehle erkennen und zu einem reduzieren müssen. Also falls die VM einen Befehl für "a=a+1: If a>b then,,," hat, dann sollte auch die Sprache 1 Befehl mit entsprechenden Parametern haben und nicht diese Combo aus LET und IF.

    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.

    Schnell genug sicher. Imho eher eine Frage des Scopes und wie sich Variablen nicht in die Quere kommen. Muss es parallel laufen können, z.B. Tastaturabfrage? Oder darf es Entweder-Oder sein, also entweder das eigene Programm, oder Editor und andere Funktionen?

    Ich durchblicke hier nicht, was der Gedanke dahinter ist?
    Irgendwie alles mögliche auf dem Stack haben zu können, incl. Kommandos wie +, die sonst sofort ausgeführt würden?
    + 1 5 heißt doch als Kommandos beschrieben:
    Push 5, Push 1, +(das dann die oberen beiden Elemente durch die Summe ersetzt), es bleibt eine 6 auf dem Stapel.
    Push 5, Push 1, Push +, execute verstehe ich auch noch. Falls man irgendwie den ganzen Stapel noch zwischenspeichern will oder so.
    Verstehe ich die Schreibweise überhaupt richtig?

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


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

    Also ich bin da echt nicht von überzeugt, alles auf dem Stapel abzulegen, incl. Datentypen und Variablennamen. Das sieht nach einem riesigen Aufwand aus, Variablen zu finden. Und wie ist das mit Strings variabler Länge und einer Garbage collection?

    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.

    Das erinnert mich ein bisschen an Cobol. Das hatte eine Data Division, in der alle Variablen eher wie in Assembler definiert werden. Da konnte an Position 0 ein String A$ von 11 Zeichen stehen und mit "Hallo " vorbelegt werden, ab Stelle 7 String ANREDE$ (für Herr/ Frau). A$ hat also immer beide gemeinsam angesprochen.


    Dieses Cobol-Feature macht mit variabler Länge keinen Sinn mehr. Aber wenn man für die Sprache explizite Variablen-Deklaration vorsieht und das Definieren vom übrigen Editor abtrennt, dann könnte man schon eine Art von Struktur billig machen.


    Ich sehe hier nur seltsame Zeichen und Worte :nixwiss:


    Der ":" ist eine Art Endezeichen des Labels "fac", und das "=" ist ein Stackkommando für den Vergleich mit dem folgenden Wert?
    Oder doch ":=" als Zuweisung wie in Pascal?
    "->" holt den letzten Stackwert, springt bei "FALSE" zum & (oder ;), bei TRUE führt es den Ausdruck aus?


    "fac" ist der Funktionsname, hier mehr so eine Art Label?
    "fac 3" ist der Aufruf von fac, der vorher eine 3 auf den Stapel legt?
    Danach kommt "0" auf den Stapel, das Kommando "=" vergleicht die oberen Elemente (0 und 3) , gibt "FALSE" auf den Stack und verwirft die 0?
    "->" holt das false runter und springt zum &?
    "(int n)" versucht, den letzten Stapelinhalt dem Integer zuzuweisen und legt ein "TRUE" auf den Stack?


    Und wie passt das zu "X >= 5; verhält sich wie eine "Assertion", also bricht der Block ab, falls X kleiner 5 ist." ?
    Müsste das dann nicht ">= X 5" sein?


    Aber wozu ist das alles gut? Ich durchblicke die nützliche Idee dahinter nicht.
    Für Menschen sieht es mir schwer verständlich aus, wäre also nicht so schön als Basic-Ersatz.
    Noch scheint es wegen der Reihenfolge und Sonderfälle für die Maschine einfach abzuarbeiten.
    Wieso schreibt man diese Stack-Sachen eigentlich von rechts nach links?


    Eigentlich nehme ich jetzt nur mit, dass Du ein Switch/Case haben willst und dass das irgendwie auch mit Stack auszudrücken ist, aber eine Vorstellung der VM-Befehle oder der eigentlichen Sprache hab ich nun nicht bekommen.

  • Identifier sind Variablennamen, Funktions/Labelnamen und so?

    Genau!

    Im Grunde richtig! Damit habe ich gemeint, dass Operationen und Funktionen selber als Werte (bzw. Referenzen darauf) auf dem Stack liegen können, und nicht sofort mit dem Stackinhalt ausgeführt werden, sondern als Werte behalten und z.B. an andere Funktionen übergeben werden können.

    Also ich bin da echt nicht von überzeugt, alles auf dem Stapel abzulegen, incl. Datentypen und Variablennamen. Das sieht nach einem riesigen Aufwand aus, Variablen zu finden. Und wie ist das mit Strings variabler Länge und einer Garbage collection?

    Nein, das war schlicht und einfach von mir nicht gut beschrieben. In den Beispielen habe ich versucht, anzudeuten, wie der Stackinhalt aussieht. Es war schon so gedacht, dass die nackten Werte draufliegen. Die "x=" haben dabei eher den Sinn, beim Lesen daran zu erinnern, dass es diese und jene Variable ist, die dort liegt. Allerdings hat M.J, mittlerweile gute Argumente geliefert, den Evaluations-Stack und den Daten-Stack (den mit den Variablen drauf) zu separieren, also ist es vielleicht anders viel besser.

    Ich sehe hier nur seltsame Zeichen und Worte

    Verständlich :-). Ich hab ungefähr versucht, die Abarbeitungsreihenfolge darzustellen, und als Ausdruck in jedem Schritt stehen zu lassen. War nicht sonderlich lesbar, und leidet darunter, dass ich da wirklich vor mich hin phantasiert habe. Zwar würde es so funktionieren, wie ich es gemeint habe, aber die Beschreibung selber ist viel zu kryptisch.


    Eigentlich nehme ich jetzt nur mit, dass Du ein Switch/Case haben willst und dass das irgendwie auch mit Stack auszudrücken ist, aber eine Vorstellung der VM-Befehle oder der eigentlichen Sprache hab ich nun nicht bekommen.

    Das ist die Folge davon. Und ich wollte eigentlich mit einem Konstrukt viele andere abstrahieren. Ja, auch switch, auch (anonyme) Funktionen, auch if..then..else, etc. Aber das gehört noch einmal durchdacht, da bin ich definitiv zu weit abgehoben, wobei es ja nicht grundsätzlich falsch ist. Aber in dem Punkt hast du völlig recht: Die VM wird schlecht bis nicht beschrieben, und dadurch ist nichts wirklich verständlich. Zudem kann das darauf hinweisen, dass es schlecht duchdacht ist! Das heißt, dein Feedback sagt vielleicht mehr aus, als dir bewusst ist: Es liegt höchstwahrscheinlich nicht an dir, sondern tatsächlich auch teilweise an dem, was ich da zusammenfabuliert habe! ;-)

  • Zu dem Bubblesort-Beispiel von M.J., das ich noch einmal hierher schreibe, weil das Orignial schon weit nach oben gerutscht ist:




    Es ist tatsächlich ein extrem guter Denkanstoß, weil es viele Fragen gleichzeitig aufwirft, die eigentlich alle gelöst sein wollen, bevor es überhaupt konsistent formulierbar ist.


    Zum Beispiel erhebt sich die Frage, ob und wie weit eine Typ-Überprüfung in so einem kleinen Interpreter möglich ist, und wenn, ob sie dynamisch oder statisch, und ob sie streng oder eher als Empfehlung erfolgen sollte, wie ein vorhandenes Array dargestellt und angesprochen wird, wie seine Elemente angesprochen werden, etc.


    Wie immer sind die hier gewählten Lösungen temporäre Vorschläge. Ich nehme einmal an, dass (Achtung, Baustelle!) ein Array zur "Experimentier-Zeit", also während man ein Programm on board erstellt, als Triple einer Addresse, eines Element-Typs und einer Größe vorliegt. Wie man zu dieser Dastellung gelangt, wie sie intern aussieht, nehme ich noch nicht vorweg.


    Wichtig ist, dass diese Informationen beim Aufruf der Funktion auf dem Daten-Stack vorliegen, und nicht überschrieben werden.


    Weiters braucht es jeweils eine Möglichkeit zur Address-Berechnung eines bestimmten Elements in dem angegebenen Speicherbereich, und unabhängig davon eine Lese bzw. Schreiboperation auf diese Addresse. Also fragt sich als nächstes, wie weit diese Addressberechnungen bereits abstrahiert sind. Man hat hier im Grunde die Bandbreite zwischen der abstrakten high level Sichtweise wie in höheren Programmiersprachen, und einer maschinennahen Berechnungs-Sichtweise, die eine VM implizieren würde. Wo findet die statt? Im Compiler? In der VM? In vorhandenem Bytecode? Im gefragten Code selbst? Auch das will mit einiger Sensibilität geklärt sein, und so weit bin ich noch nicht wirklich. ;-)


    Ich gehe davon aus, dass es diese Berechnung grundsätzlich gibt, und es notwendig ist, sie in einer dynamischen Programmierumgebung zunächst auch mindestens dynamisch (zur Laufzeit) vorzunehmen. Für den Source-Code bedeutet das, dass es bereits Sprachelemente dafür gibt, unabhängig von ihrer Implementierung. Da die Größe des jeweiligen Elements durch seinen Typ festgelegt ist, braucht sie in der Address-Beschreibung nicht mehr erwähnt zu sein (in der Berechnung muss sie natürlich vorkommen, aber das reflektiert der Programm-Text in dem Fall nicht).


    Ich benutze nicht die übliche C-Notation der eckigen Klammern (die ist schon besetzt), sondern das Kanalgitter "#Index", um einen Index zu beschreiben. Weiters benutze ich die Referenzierungs-Notation "Adresse^" um eine Basis-Adresse zu beschreiben. Das führt zu "Adresse^#Index" als Ausdruck der Addressberechnung. Ein Zugriff würde dann sinngemäß über "Adresse^#Index^" erfolgen, und im Code durch (schreibend) "Adresse^#Index := Wert" oder (lesend) "Wert := Adresse^#Index" symbolisiert.


    (Interessant dabei dass ich hier von der Präfix-Notation abweiche, und deshalb auch gerade überlege, ob es generell sinnvoll wäre, eine Infix-Notation einzuführen, oder ob es besser wäre, eine Präfixnotation stattdessen zu nehmen. Aber das ist ein Nebenschauplatz und keineswegs sicher, ich werde das für dieses Provisorium nicht endgültig lösen).


    Stattdessen - um die Lesbarkeit dieses Beispiels zu erhöhen - benutze ich einmal frech eine Infix-Notation.



    Hier dasselbe noch einmal ohne Kommentare:



    Nachtrag:


    Dank an J.M, der nicht müde wird, mich immer wieder auf die Problematik der Übersetzung aufmerksam zu machen, die so trivial nicht ist: Wieviele und welche Tokens hätten wir da drin?


    • Zwei paar Klammern, runde und spitze
    • Eine Zuweisung :=
    • Zweimal "^" als Referenzierung und Dereferenzierung
    • "inc" und "dec" als Instruktionen
    • Zwei Vergleiche ">>" und "<<" (wobei ich die einfachen Symbole bereits reserviert habe, deshalb die doppelten).
    • Den funktionalen Pfeil ->
    • Zwei Separatoren "," und ";" (wobei letzteres auch ein Terminator ist).
    • Eine "repeat"-Instruktion
    • "#" als indizierungs-Operator
    • Zwei Typen "int8" und "int16"
    • Die integer-Zahl 0

    Macht, falls ich nichts vergessen habe, 16 verschiedene Tokens. Dieses Programm nutzt lange nicht alle, so dass man getrost von einer Gesamtzahl von über 32 Tokens ausgehen kann. Vielleicht lassen sie sich auf 64 begrenzen, allerdings sind dann sämtliche Typ (Bitlängen)-Informationen der Operationen noch nicht drin. Somit ist es bereits fraglich, ob sich das mit einem Byte ausgeht, bzw. kann man sich überlegen, welche Informationen man jeweils in einem weiteren Byte unterbringen wird müssen.



    PS: Wieso kann man im Code-Editor eigentlich keine Tabs tippen? Reinkopieren kann man sie nämlich sehr wohl!






    Wie auch bisher: Die Syntax ist reine Improvisation!

  • Nun denn, da habe ich wohl die undankbare Aufgabe, mit besserwisserischem Gemecker daherzukommen... :/


    Zitat

    Zum Beispiel erhebt sich die Frage, ob und wie weit eine Typ-Überprüfung in so einem kleinen Interpreter möglich ist, und wenn, ob sie dynamisch oder statisch, und ob sie streng oder eher als Empfehlung erfolgen sollte, wie ein vorhandenes Array dargestellt und angesprochen wird, wie seine Elemente angesprochen werden, etc.

    Auf einem C64 stellt sich diese Frage eigentlich nicht, es sei denn, Du willst, daß die Benutzer Deiner Sprache vor dem Computer einschlafen. Auch die schlechtesten BASIC-Compiler sind in der Lage, V2-Basic-Programme zu beschleunigen, weil sie
    1.) konstante Zahlen von der Zifferdarstellung in Binärwerte umwandeln,
    2.) die Referenzierungen von Variablen in Direktzugriffe auflösen,
    3.) die Typüberprüfung abschaffen,
    4.) Sprünge direkt ohne Suchen vornehmen,
    5.) Ausdrücke von der Infixnotation in die Postfixnotation umwandeln.


    Bezogen auf Deine Sprache bedeutet dies:


    1.) Zur Zahlendarstellung sagst Du, daß Zahlen direkt als Zahlenwerte abgelegt sein sollen. Nehmen wir das als gegeben an.
    2.) Zu der Organisation von Variablen hast Du noch keine Angaben gemacht.
    3.) Dynamisch ist so ziemlich das lahmste, was man auf dem C64 anstellen kann. Gut, wenn man zwischendurch Kaffeetrinken oder eine Runde joggen will. Für ernsthafte Programme jedoch ungeeignet.
    4.) In Deinem Beispielprogramm baust Du Schleifenkonstrukte mit der Hilfe von Blöcken gekennzeichnet durch []. Am Ende einer solchen Klammerung findet sich dann eine Bedingung gefolgt von einem Sprung "->repeat". Schreibt man dies als Basicprogramm, so lautet die Schleife:

    Code
    1. I = 0
    2. LOOP
    3. ...
    4. I = I + 1
    5. IF I < (N - 1) THEN LOOP

    Mit anderen Worten: "-> repeat" ist nichts weiter als ein GOTO-Sprung. Damit verfügt Deine Sprache also schon mal nicht über eine FOR-Schleife. Es ist davon auszugehen, daß auch andere Schleifen (WHILE, REPEAT) auf diese Weise aufgelöst werden. Noch anders gesagt: Deine Sprache ist vom Abstraktionsgrad her noch unter BASIC anzusiedeln. Persönlich habe ich nichts gegen die gelegentliche Verwendung von GOTO in einem Hochsprachenprogramm, wenn es dazu dient, anderweitig umständlichen Code zu vermeiden. Als Kernelement einer Sprache möchte ich GOTO aber nicht mehr sehen. In dieser Hinsicht ist man inzwischen weiter.
    Ein Problem dabei ist zudem, daß in Deinem Fall anders als in BASIC das Sprungziel nicht direkt im Code angegeben wird. Woher weiß also der Interpreter, wohin er bei einem "repeat" springen muß? Das geht nur, wenn er ähnlich wie der BASIC-Interpreter einen Sprungstack mit sich herumschleppt, auf dem er zu Beginn der Schleife die Anfangsadresse ablegt. Die Verwaltung dieses Sprungstacks erfordert nicht nur einen Spezialinterpreter (Ade universaler Interpreter), sondern auch wieder viel Zeit.
    Die Vermeidung von direkten Sprungzielen führt jedoch auch dazu, daß break-Anweisungen zum vorzeitigem Austritt aus der Schleife nicht möglich sind. In BASIC kann eine Schleife mittels "GOTO zeile" selbstverständlich vorher verlassen werden, denn wenn man das Programm nicht allzu ungeschickt gestaltet, wird der Sprungstack irgendwann automatisch wieder korrigiert. In Deinem Fall ist dies nicht möglich, weil der Interpreter gar nicht wüßte, wohin er springen soll, es sei denn, Du integrierst eine Suchroutine, die den folgenden Tokenstrom darauf untersucht, wo dieser Block zuende ist. Auch das kostet natürlich wieder viel Zeit. Besser wäre es, wenn der Interpreter vorher schon das Ende wüßte, doch das würde voraussetzen, daß Du von der inkrementellen Idee abrückst.
    Wie man an dem Beispiel sieht, kann vorher nicht gesagt werden, wie lang der Block insgesamt wird. Allein in diesem Beispiel erstreckt er sich schon über mehrere Zeilen. Außerdem können Blöcke wiederum Blöcke beinhalten, was ein rekursives Parsen erfordert. Wo man dann die Grenze ziehen will zwischen einem Block für die inkrementelle Übersetzung und dem ganzen Programm (auch ein Block) erscheint mir allzu willkürlich.
    Zuletzt verwendest Du im Block auch noch lokale Variablen, die Du mittels int8 zur Laufzeit erzeugen läßt. Da wird das Joggingerlebnis zum Marathon. Nicht nur, daß so in der i-Schleife die Variable j bei jedem Durchlauf neu angelegt werden muß, sie muß nach Abschluß der j-Schleife auch jedes Mal vom Stapel entfernt werden. Das bedeutet, daß bei jedem ']' der Interpreter schauen muß, ob irgendwelche Variablen angelegt worden sind, um diese gegebenenfalls zu verwerfen. Nun kann man das natürlich automatisieren, indem man einen Stackframe anlegt für jede Schleife, so daß am Ende "nur" der alte Stapelzeiger restauriert werden muß (vgl. LINK und UNLK beim 68000), doch kann man jetzt schon sagen, daß dieser Aufwand in keinem Verhältnis mehr zum Nutzen steht. Am Ende mag man sich gar nicht ausmalen, was passieren soll, wenn der Interpreter mitten in solch einer verschachtelten Schleife auf den Befehl "return", also dem vorzeitigen Abbruch der Unterroutine, trifft. Das Gefrickel mit dem Abwickeln der Stackframes und des Sprungstacks möchte ich nicht implementieren.
    Kurz: Implizite Sprünge ohne klares Sprungziel und Interpreter sind zwei Dinge, die sich gegenseitig ausschließen. Nicht ohne Grund gibt es kaum Pascal- oder C-Interpreter. Und auch diese arbeiten nicht inkrementell, sonder basieren auf einer vorausgehenden Volltextanalyse. Die Verrenkungen, die nötig sind, um die impliziten Sprünge aufzulösen, sind viel zu umständlich und nervig, als daß sich ein Einsatz als Interpreter lohnen würde.
    5.)

    Zitat

    Interessant dabei dass ich hier von der Präfix-Notation abweiche, und deshalb auch gerade überlege, ob es generell sinnvoll wäre, eine Infix-Notation einzuführen, oder ob es besser wäre, eine Präfixnotation stattdessen zu nehmen. Aber das ist ein Nebenschauplatz und keineswegs sicher, ich werde das für dieses Provisorium nicht endgültig lösen

    Das ist leider kein Nebenschauplatz, sondern elementar für die Sprache. Du wirst Dich für eine Notation entscheiden müssen. Was ergäbe sich daraus?
    a) Postfix ("a 4 +")
    Nachteil: Schlecht zu lesen. Warum würde man sowas nehmen wollen? Eine Sprache sollte sich am Menschen orientieren, nicht an der Maschine. Bei FORTH als eine historische auf Kleinstrechnern entstandene Sprache kann ich es noch akzeptieren. Für eine Neukreation jedoch stellt sich die Frage: Wieso um alles in der Welt?
    b) Infix ("a + 4")
    Jede Infixnotation muß zur Abarbeitung umgewandelt werden in eine Postfixnotation. (Lade erst die ALU mit den beiden Werten und führe dann die Addition aus.) Die Frage ist, wann das geschieht.
    - Zur Laufzeit:
    Schnarch... (vgl. BASIC)
    - Bei der Umwandlung in Token:
    Tja, kann man machen, aber man sollte dabei nicht vergessen, daß die Token später wieder in Infix umgewandelt werden müssen. Beispiel:
    infix: a / (i + 3)
    postfix: a i 3 + /
    Blöd ist, daß die Klammer in der Postfixschreibweise verschwindet. Die muß man also wieder ergänzen. Das macht die Umwandlung etwas umständlich, denn man kann nicht einfach sequentiell vorgehen, sondern muß den Ausgabestring rekursiv erzeugen. Keine leichte Sache auf dem C64.
    infix: a + (i + 3)
    postfix: a i 3 + +
    Hier hat der Programmierer absichtlich zur Verdeutlichung des Codes eine zusätzliche Klammer gesetzt, die leider beim Umwandeln in Postfix auch verschwindet. Soll jetzt der Tokenizer erkennen, daß diese Klammer absichtlich überflüssig gesetzt war und dann ein Sondertoken dazwischen schieben, daß der Interpreter jedes Mal brav überspringen darf?
    Und was passiert eigentlich, wenn i nicht eine Variable, sondern eine Konstante wäre?
    a / (KONST + 3)
    Eine der einfachsten und gleichzeitig effektivsten Optimierungen, die ein Compiler vornehmen kann, ist Constant folding (https://en.wikipedia.org/wiki/Constant_folding), das Berechnen von Ausdrücken mit konstanten Elementen durch den Compiler. Da Du aber keinen Compiler hast bzw. konstante Ausdrücke nicht einfach auflösen kannst, weil dem Programmierer sonst der Code zerstört wird, müssen Deine Programme auf derartige Optimierungen komplett verzichten. Nun kann man zwar anführen, daß man nebenbei noch einen Compiler haben kann, der all dies und noch mehr macht, doch frage ich mich dauernd: Wozu brauche ich zwei verschiedene Dinge, Interpreter und Compiler, für ein und dieselbe Sache? Und wenn man dann später ohnehin einen Compiler anwerfen muß, um das Programm brauchbar schnell zu machen: Wozu benötigt man dann einen total komplizierten Tokenizer für die Programmentwicklung? Wozu braucht man dann eine Sprache, die sich am untersten Level der Maschine orientiert?


    Gesamtfazit: Ich meine es nicht böse, aber zur Zeit sieht Deine Sprache weniger wie eine Hochsprache aus als viel mehr wie eine Assemblersprache für eine Stackmaschine. Auch wenn Deine Syntax anders gestaltet ist mit äußerlich verschiedenen Symbolen, sind die Ähnlichkeiten zu z. B. CIL (vgl. https://en.wikipedia.org/wiki/Common_Intermediate_Language bzw.
    https://en.wikipedia.org/wiki/List_of_CIL_instructions) unübersehbar. Letzteres ist jedoch bereits ein Kompilat. Die Frage stellt sich, warum man heute und insbesondere auf dem C64 in einer Lowlevelsprache programmieren soll, wenn dann 6502-Assembler um vieles schneller ist. Nach einer einfachen für jedermann zu benutzenden Programmiersprache wie BASIC sieht es für mich persönlich bislang nicht aus. Zudem vermute ich mal, daß 99% der Leute ohnehin schon "funktionale Programmiersprache" und "einfach loslegen" für ein Paradoxon halten. Noch hat Dein Sprachansatz einige Design-Flaws, und bei z. B. Sprüngen und Stackframe bin ich nicht überzeugt, daß es Dir gelingen wird, diese zu beseitigen. Aber wer weiß. Ich kann mich ja irren.

  • Nun denn, da habe ich wohl die undankbare Aufgabe, mit besserwisserischem Gemecker daherzukommen...

    .. für das ich nebenbei extrem dankbar bin, wie auch für die Mühe, die du dir damit machst!

    Auf einem C64 stellt sich diese Frage eigentlich nicht, es sei denn, Du willst, daß die Benutzer Deiner Sprache vor dem Computer einschlafen.

    Es ist dann nötig, wenn ich dynamische Definitionen habe (die sind ja der Witz am "loslegen und Spaß haben"), die zur "Compile-Zeit" geprüft werden sollten, bzw. noch schlimmer (jetzt wirds Zeit für Baldrian-Tropfen :D ) wenn ich tatsächlich sowas wie ein dynamisches Typsystem haben will (das ausdrucksstärker ist als ein statisches, darum gehts!)

    Mit anderen Worten: "-> repeat" ist nichts weiter als ein GOTO-Sprung.

    Das würde ich nicht unterschreiben. Es ist ein Sprung an den Anfang des Blocks, und damit ist der Stack (und alle Scopes) soweit wohldefiniert. Das Destruktive am GOTO ist ja eher, dass es eventuell einen Großteil des verlässlichen Programmzustands wegräumt. Ob die Initialisierung vor die Schleife oder in einen Header formuliert wird, ob der Schleifenschritt im Header oder im Body steht, ist konzeptuell nicht so wichtig, sondern eher syntaktischer Zucker.

    Woher weiß also der Interpreter, wohin er bei einem "repeat" springen muß?

    Das ist eine Herausfordrung, danke für den Hinweis. Da es absoluter Irrsinn wäre, das dynamisch rauszufinden, wirds wohl im Compile-Schritt stattfinden müssen.

    Die Vermeidung von direkten Sprungzielen führt jedoch auch dazu, daß break-Anweisungen zum vorzeitigem Austritt aus der Schleife nicht möglich sind

    Ah doch, wenns vorher entsprechend "compiliert" wird. Allerdings könntest du antworten, dass man ja doch die Information zur Rückverwandlung beibehalten muss, und du hättest recht. Eine reine "Goto"-Variante auf VM-Ebene wirds also zunächst nicht werden, aber zumindest eine, die es viel schneller kann. Aber da wird noch einiges an Gedankenarbeit rein rinnen.

    Wo man dann die Grenze ziehen will zwischen einem Block für die inkrementelle Übersetzung und dem ganzen Programm (auch ein Block) erscheint mir allzu willkürlich.

    Die ergibt sich für mich daraus, dass kleine Einheiten (z.B. Funktionen) unabhängig voneinander definiert und editiert werden können, eben nicht das Ganze auf einmal, und aus einer noch zu überlegenden Größenbeschränkung.

    Zuletzt verwendest Du im Block auch noch lokale Variablen, die Du mittels int8 zur Laufzeit erzeugen läßt.

    Das ist keine "Erzeugung", sondern da wird bloß Platz auf dem Datenstack angenommen. Das läuft statisch ab.

    Nicht nur, daß so in der i-Schleife die Variable j bei jedem Durchlauf neu angelegt werden muß, sie muß nach Abschluß der j-Schleife auch jedes Mal vom Stapel entfernt werden. Das bedeutet, daß bei jedem ']' der Interpreter schauen muß, ob irgendwelche Variablen angelegt worden sind, um diese gegebenenfalls zu verwerfen.

    Das verstehe ich jetzt nicht. Im Grunde entsprechen diese Variablen bloß Daten-Stack-Positionen. Die Identifier werden 1:1 in Addressen übersetzt. Irgendwo ausserhalb gibt es eine Tabelle, die diese Zuordnung enthält, und ggf. zur Rückübersetzung konsultiert wird (die gäbs im Compilat nicht mehr).

    Am Ende mag man sich gar nicht ausmalen, was passieren soll, wenn der Interpreter mitten in solch einer verschachtelten Schleife auf den Befehl "return", also dem vorzeitigen Abbruch der Unterroutine, trifft.

    Nichts. Er hüpft aus der Funktion heraus, und die aufrufende Funktion weiß immer noch ganz genau, wie ihr Stack aussieht. Falls etwas zurückgegeben wird, befindet es sich auf dem Evaluationsstack, würde ich einmal sagen (oder hast du einen Grund, davon abzuraten?).

    doch frage ich mich dauernd: Wozu brauche ich zwei verschiedene Dinge, Interpreter und Compiler, für ein und dieselbe Sache?

    Weil der Compiler entweder auf einem PC läuft, oder so langsam ist, dass er zum "Spaß haben" völlig ungeeignet wäre. Wobei - ich habe Verständnis dafür, dass du das Konzept in Frage stellst. Eine ganz normale Hochsprache, die PASCAL- oder C-gleich einfach schnellen Code erzeugt, hat ihren Reiz in dem Kontext! Bloß wäre sie das, was hier als "Doping" bezeichnet würde, weil sie kaum mit 16k ROM auskommt.
    ;-)

    Die Frage stellt sich, warum man heute und insbesondere auf dem C64 in einer Lowlevelsprache programmieren soll, wenn dann 6502-Assembler um vieles schneller ist.

    Aus meiner Sicht ist die Darstellung, wie ich sie gewählt habe, ungleich lesbarer als jeder Assembler-Code, weil diese Sprache erheblich mehr Komfort bietet, etc.

  • Ah doch, wenns vorher entsprechend "compiliert" wird.

    Bislang sprachst Du stets davon, daß der Code als Tokenstrom interpretiert und nicht vollständig kompiliert werden soll. An anderer Stelle hatte ich Dir bereits geschrieben, daß bei einem Compiler die Abbildung von Quellcode auf Zielcode surjektiv ist. Du verlangtest jedoch einen bijektiven Code, d. h. eine 1:1 Entsprechung von Klartext und Tokenstrom, damit ein gegenseitiges Umwandeln möglich wird. Das ist aber nicht Kompilieren, sondern nur Tokenisieren. Solange Du auf dem 1:1-Verhältnis beharrst, wird das nichts mit dem Compiler, und alle Einwände auf Grundlage des Nichtvorhandenseins eines Kompilats (Sprünge etc) bleiben weiter gültig. Um Sprünge auflösen zu können, mußt Du Dich vom rückumwandelbaren Tokenstrom verabschieden.

    Ob die Initialisierung vor die Schleife oder in einen Header formuliert wird, ob der Schleifenschritt im Header oder im Body steht, ist konzeptuell nicht so wichtig, sondern eher syntaktischer Zucker.

    Öhm... Das Vorhandensein einer expliziten FOR-Schleife würde ich jetzt nicht wirklich als "syntactic sugar" bezeichnen wollen.

    Das ist keine "Erzeugung", sondern da wird bloß Platz auf dem Datenstack angenommen. Das läuft statisch ab

    Nichts. Er hüpft aus der Funktion heraus, und die aufrufende Funktion weiß immer noch ganz genau, wie ihr Stack aussieht.

    Ich befürchte, Du hast das Konzept eines Stapels nicht vollständig durchdrungen. Stell Dir eine Prozedur p vor mit der lokalen Variable L. Zu der lokalen Variable L gibt es keinen absoluten Speicherort, sondern die Position dieser Variable wird relativ zum Stapelzeiger gemessen. Ruft das Hauptprogramm die Prozedur auf, wird der Stapelzeiger runtergesetzt. Dadurch wird einerseits Platz geschaffen für die lokalen Variablen, andererseits kann man diese nun mit Hilfe des Stapelzeigers als "Stapelzeiger + relativer_Offset" ansprechen.
    Wird dieselbe Prozedur jedoch aus einer anderen Prozedur q heraus aufgerufen, befindet sich die Variable L nicht an der gleichen Stelle wie vorher, sondern die Prozedur q hatte vorher bereits ihren Platz auf dem Stapel rerserviert und den Stapelzeiger heruntergesetzt. Nur so ist es möglich, daß auch bei verschachtelten Aufrufen immer wieder freier Platz für lokale Variablen vorhanden ist, der sich ncht mit anderen überschneidet.
    Dies macht es aber auch erforderlich, daß der Stapelzeiger bei Ende der Prozedur auf den alten Zustand vor Aufruf der Prozedur zurückgesetzt wird, da sonst der Stapel bei wiederholtem Aufrufen der Prozedur unweigerlich überlaufen würde.
    In kompiliertem Pascal oder C kann der Compiler berechnen, wieviel Platz alle lokalen Variablen (auch die, die irgendwo mittendrin erzeugt werden) erfordern, und kann Code generieren, der diesen Platz zu Beginn der Prozedur reserviert. Diese Information hat Dein System aber nicht (, wenn es nicht kompiliert wird).
    Wenn Du nun mitten in einer Prozedur eine neue Variable anlegst, mußt Du entsprechenden Platz auf dem Stapel erzeugen. Daran führt kein Weg vorbei. Machst Du das nicht, bekommst Du arge Probleme, wenn Du z. B. innerhalb der Schleife eine weitere Prozedur r aufrufst. Diese Prozedur hat wiederum ihren eigenen Stapelbereich, der zu Beginn angelegt wird. Versäumst Du, für Deine lokale Variable L den Stapelzeiger zu erniedrigen, wird die Prozedur r den Inhalt Deiner Variablen L gnadenlos überschreiben. Es reicht nicht, sich irgendwo die relativen Offsets auf den Stapel zu merken, wenn nicht gleichzeitig Platz für diese geschaffen wird.
    Ein weiteres Problem besteht darin, daß bei der Initialisierung von i zwar der relative Offset auf den Stapelzeiger ermittelt werden kann. Wenn anschließend jedoch der Stapelzeiger erneut verändert wird, um Platz zu schaffen für die neue Variable j, stimmen die relativen Offsets nicht mehr.
    Ich kann Dir nur den dringenden Rat geben, etwas über Stapelverwaltung (angefangen hier) zu lernen, damit Du die Dimension begreifst, was es bedeutet, in einem Programm für jeden Block einen eigenen Stackframe anzusetzen. Nimm danach bitte einen Zettel und Bleistift und versuche, die Verwaltung des Stapelzeigers nachzuvollziehen (bitte dabei nicht Datenstack und Evaluationsstack verwechseln), indem Du für das innere der j-Schleife wahlweise annimmst, daß dort ein Unterprogrammauf, eine break-Anweisung oder eine return-Anweisung steht.

    16k ROM

    Genau gesagt sind es nicht einmal 16k ROM, sondern nur der Teil des Roms, der bislang vom Basic-Interpreter gebraucht wird. In dieses ROM soll nun ein Editor/Tokenizer/Detokenizer/Interpreter gequetscht werden? Denk bitte daran, daß Deine Sprache nicht auf Einzelzeilen aufbaut wie V2-BASIC. Du brauchst also einen neuen Editor dafür. Tokenizer und Detokenizer müssen in der Lage sein, die Bezeichner irgendwie getrennt zu verwalten. Und der Interpreter soll in der Lage sein, zwischen int8 und int16 unterscheiden zu können. Und am besten noch alles dynamisch, so daß ein Bezeichner wahlweise eine 16-Bit-Integerzahl sein kann als auch eine Zeichenkette als auch ein zweidimensionales Feld von Fließkommazahlen als auch eine Verbundstruktur aus all den Sachen zusammen... Ist das wirlich realtisch? :/

  • Du verlangtest jedoch einen bijektiven Code, d. h. eine 1:1 Entsprechung von Klartext und Tokenstrom, damit ein gegenseitiges Umwandeln möglich wird. Das ist aber nicht Kompilieren, sondern nur Tokenisieren.

    Nein, sondern ein Naheverhältnis. Dass 1:1 nicht geht, ist klar.

    Solange Du auf dem 1:1-Verhältnis beharrst, wird das nichts mit dem Compiler, und alle Einwände auf Grundlage des Nichtvorhandenseins eines Kompilats (Sprünge etc) bleiben weiter gültig. Um Sprünge auflösen zu können, mußt Du Dich vom rückumwandelbaren Tokenstrom verabschieden.

    Das würde ich nicht so sehen. Ich kann ja diese Information beibehalten, und trotzdem die Möglichkeit effizienterer Rück- und Vorwärtssprünge vorsehen (1:1 habe ich nie gefordert, sondern eben "fast" 1:1. Falls es doch so irgendwo in meinen Texten steht, bitte ich um Verzeihung für das Mißverständnis). Da sind wir wieder an der Stelle, an der ich sage: Es gibt Informationen, die man erst beim reinen Compilat wegwerfen darf, und während der interaktiven Entwicklung beibehält.

    Öhm... Das Vorhandensein einer expliziten FOR-Schleife würde ich jetzt nicht wirklich als "syntactic sugar" bezeichnen wollen.

    Ich schon, ich sehe das von einer anderen Warte her. Es ist wichtig, dass grundsätzlich die Block-Struktur beibehalten, und nicht aufs geratewohl irgendwohin gesprungen wird. Das unterscheidet das GOTO von den logisch viel besser fassbaren Block-Sprüngen.

    Wird dieselbe Prozedur jedoch aus einer anderen Prozedur q heraus aufgerufen, befindet sich die Variable L nicht an der gleichen Stelle wie vorher,

    Klar.

    Dies macht es aber auch erforderlich, daß der Stapelzeiger bei Ende der Prozedur auf den alten Zustand vor Aufruf der Prozedur zurückgesetzt wird, da sonst der Stapel bei wiederholtem Aufrufen der Prozedur unweigerlich überlaufen würde.

    Ja, natürlich. Darin sehe ich aber auch kein Problem.

    In kompiliertem Pascal oder C kann der Compiler berechnen, wieviel Platz alle lokalen Variablen (auch die, die irgendwo mittendrin erzeugt werden) erfordern, und kann Code generieren, der diesen Platz zu Beginn der Prozedur reserviert. Diese Information hat Dein System aber nicht (, wenn es nicht kompiliert wird).

    Das kann man in dem Fall auch. So enorm schwer ist es nicht, sich während der Tokenisierung konstante Offsets zu merken, und klar, das ist bereits ein kleiner Compile-Vorgang, den ich ja nicht geleugnet habe. Die Kunst besteht darin, ihn möglichst klein zu halten, nicht ihn ganz wegzulassen.

  • Ich machs einmal konkreter:


    Der .. wie nennen wir ihn? .. Mini-Compiler (der so Mini nicht ist, seine Objekte sind mini) braucht für die Block-Struktur drei Offset-Informationen:


    1. Die Argumente der Funktion. Hier frage ich mich grade, ob die daher irgendwie vom Evaluations-Stack auf den Daten-Stack gelangen müssen, wo sie als Parameter für die aufgerufene Funktion wie lokale Variablen wirken.
    2. Den Beginn der lokalen Variablen, die in der Funktion definiert sind.
    3. Jeweils (abhängig von der Block-Struktur) das Ende der lokalen Variablen.


    Zudem braucht er für jede verwendete lokale Variable den Offset (vom Beginn des Bereichs).


    Man könnte das wohl auch umgekehrt sehen, vom Ende des Variablenbereichs her, oder wie auch immer. Das ist ein Detail. Wichtig ist, dass diese Informationen grundsätzlich da sein müssen und im Programm codiert sind.


    Jeder Block eröffnet wieder zwei zusätzliche Informationen: Wo er beginnt, und wo er endet. Theoretisch könnte man das auch viel einfacher machen, in dem man aus jedem Block einen eigenen "Code-Rucksack" macht, auf den hingesprungen wird, so dass ein "repeat" (in C "continue") seine Anfangs-Addresse wäre, und ein "return" (in C "break") einfach die nächste Anweisung nach dem Sprung dorthin. Nachteil: Jeder Block ist mit zwei zusätzlichen Sprüngen belastet. Oder man lässt die beiden variabel, und füllt sie danach erst ein. Nachteil: Der Beginn ist unbelastet, aber der Rücksprung ein indirekter. Oder - am besten von der Performance - man rechnet sich alles aus, und füllt die Sprungbefehle am Schluss richtig aus, wenn die Informationen vorliegen. Dann dauert die Compilierung ein wenig länger, weil ein paar zusätzliche Ergänzungen notwendig sind, aber so enorm viel ist das ja nicht.


    Wie merkt man sich Block-Anfang und Ende? Mit Spezial-Tabellen ausserhalb des Codes, die bei Bedarf rausfliegen. Etwa dort merkt man sich auch die Zuordnung von Blöcken und lokalen Variablen, die man im Code alle nicht braucht, weil sie durch reine Offsets ersetzt sind.


    Wir hätten bei dem oberen Beispiel



    daher die Information:


    Der Argument-Bereich enthält

    • wahrscheinlich 2 bytes als Zeiger auf das Array (ob die Länge dann dort zu finden ist oder auch auf dem Stack übergeben wird, muss man noch überlegen).


    erster Block

    • beginnt bei Beginn1(=0) und endet bei Ende1
    • enthält i (2 Bytes) an offset 0 vom Variablenbereich
    • hat 2 Bytes Variablenbereich

    zweiter Block


    • liegt innerhalb des 1. Blocks
    • beginnt bei Beginn2 und endet bei Ende2
    • enthält alle Variablen von i



    • enthält j an Offset 2 vom Variablenbereich
    • hat 4 Bytes Variablenbereich


    (Ungefähr so, das hat jetzt keine Implementierungs-Genauigkeit)


    Im Code selber stehen diese Informationen nicht mehr. Aber beim Rückumwandeln weiß man, dass alle Sprünge im Code aufgrund dessen zustandekommen. Also übersetzt man sie entsprechend mit "repeat" oder "return". Übrigens könnte man, da vorwärts immer Blockende, und Rückwärts immer Block-Anfang bedeutet, diese Information theoretisch auch so extrahieren. Nur das kostet Zeit!


    Die Blockstruktur braucht man, um die Namen der Variablen rauszukriegen. Man kann also die gesamte Information wegwerfen, wenn man den Code nicht mehr on board editieren will, sondern er bloß ablaufen soll. Dann ist auch nicht mehr drinnen als man sich von einer ganz normalen VM an Byte-Code erwarten würde (wobei die Typ-Information in den Instruktionen selbst codiert ist).


    So ungefähr stelle ich mir das vor.

  • Nein, sondern ein Naheverhältnis. Dass 1:1 nicht geht, ist klar.

    Tut mir leid, aber ich kann Dir nicht mehr folgen. Wenn 1:1 nicht geht, besteht auch nicht mehr die Möglichkeit, den Tokenstrom nachträglich zu editieren. Das ist wie die Umwandlung von WAV nach MP3. Wenn einmal Informationen verloren gehen, bleiben sie verloren und kommen nicht mehr zurück. V2-Basic verhält sich 1:1. Deswegen kann man es so oft editieren, wie man will. Deswegen ist es aber auch langsam, erfordert direkte Sprünge und verfügt über keine Blockstruktur.

    Es gibt Informationen, die man erst beim reinen Compilat wegwerfen darf, und während der interaktiven Entwicklung beibehält.

    Es geht nicht darum, bereits vorher überflüssige Dinge wie Kommentare wegzulassen, sondern überhaupt in der Lage zu sein, Sprünge im Programm vornehmen zu können. Stell Dir eine abweisende Schleife vor, die niemals betreten wird, weil die Anfangsbedingung nicht erfüllt ist. Woher weiß der Interpreter, bis wohin er die Schleife überspringen muß?

    So enorm schwer ist es nicht, sich während der Tokenisierung konstante Offsets zu merken

    Okay, ich rate jetzt einfach mal, daß der Fehler im Verwechseln von relativen und absoluten Adressen besteht. Kann sein, daß ich falsch liege, aber es wäre besser, diesen Punkt zu klären.


    Es sei gegeben eine Prozedur p mit einer lokalen Variable L. Die Prozedur wird aufgerufen. Der aktuelle Stapelzeiger steht bei 1000.
    SP = 1000
    Bei der Programmsausführung trifft der Interpreter auf den Befehl int16. Dies veranlaßt, daß der Stapelzeiger um 2 Bytes runtergesetzt wird.
    SP = 998
    Der Wert des Stapelzeigers wird dann in der Bezeichnerliste vermerkt.
    SP = 998, L = 998
    Jedes Mal, wenn der Interpreter auf L trifft, liest und schreibt er den Wert an der Speicheradresse 998.
    Jetzt ruft die Prozedur p sich selber rekursiv auf. Für die Rücksprungadresse wird der Stapelzeiger um zwei vermindert.
    SP = 996
    Das Programm trifft wieder auf den Befehl int16. Der Stapelzeiger wird um zwei Bytes vermindert.
    SP = 994
    Der Wert des aktuellen Stapelzeigers wird unter L eingetragen.
    SP = 994, L = 994
    Gehen wir davon aus, daß es dem Programm gelingt in den ersten Aufruf zurückzuspringen und den Stapelzeiger wieder auf 998 zurückzusetzen.
    SP = 998
    Dann wurde aber der Zeiger in L nicht korrigiert und zeigt weiter auf 994. ==> Bumm
    Fazit: Es darf nirgendwo zu keiner Zeit absolute Adressen geben.


    Also das ganze nochmal von vorne:
    Es sei gegeben eine Prozedur p mit einer lokalen Variable L. Die Prozedur wird aufgerufen. Der aktuelle Stapelzeiger steht bei 1000.
    SP = 1000
    Bei der Programmsausführung trifft der Interpreter auf den Befehl int16. Dies veranlaßt, daß der Stapelzeiger um 2 Bytes runtergesetzt wird.
    SP = 998
    Der relative Offset auf den Stapelzeiger wird in der Bezeichnerliste vermerkt.
    SP = 998, L = 0
    Jedes Mal, wenn der Interpreter auf L trifft, liest und schreibt er den Wert an der Speicheradresse SP + 0 = 998.
    Jetzt trifft der Interpreter auf ein weiteres int16 für eine Variable M. Der Stapelzeiger wird um 2 vermindert.
    SP = 996. Welchen Wert soll jetzt M erhalten als relativer Offset? Welchen Wert bekommt L?
    Da sich der Stapelzeiger verändert hat, stimmt die alte Adresse für L (998 + 0) nicht mehr, denn jetzt gilt 996 + 0. Man könnte so verrückt sein und nun alle vorhergehenden Offsets korrigieren, besser aber nicht.
    Einzige Lösung: Zu Beginn der Prozedur legt man einen zweiten Stapelzeiger fest, den frame pointer. Dieser erhält bei Aufruf der Prozedur den aktuellen Stand des Stapelzeigers.
    SP = 1000, Framepointer = 1000
    Trifft der Interpreter jetzt auf den Befehl int16, wird der Stapelzeiger verringert.
    SP = 998, Framepointer = 1000
    Die Variable L erhält den negativen Offset -2, d. h. die Differenz aus framepointer und SP.
    Folgt daraufhin der zweite Int16-Befehl für die Variable M, wird der Stapelzeiger wieder um 2 verringert.
    SP = 996. M erhält den negativen Offset -4 (1000 - 996).
    Soweit so gut. Aber dieses Verfahren benötigt zwingend 2 Zeiger: Einen auf den aktuellen Stapel (SP), einen als Basisadresse auf die Variable (Framepointer). Der Framepointer muß bei jedem Aufruf der Prozedur neu gesetzt werden. Das setzt voraus, daß der alte Wert gerettet werden muß. Damit hast Du schon mal Ballast bei jedem Unterprogrammaufruf.
    Während des Programmablaufs mußt Du weiterhin den Stapelzeiger mitberechnen, da Dein Programm nicht vollständig kompiliert wird, Du also nicht weißt, wo sich Deine Variablen relativ zum Framepointer befinden. Selbst wenn Du es wüßtest, müßtest Du trotzdem den Stapelzeiger zu Beginn der Prozedur verringern, um Platz zu schaffen für Deine Variablen.
    Stößt Dein Programm nun auf das Ende eines Blocks, muß dieser Stapelzeiger auf den Wert zurückgesetzt werden vor Blockbeginn. Da Du innerhalb eines Blocks mehrere Variablen deklarieren kannst, muß der Interpreter irgendwie wissen, um wieviel er den Stapelzeiger wieder nach oben setzen muß. Woher weiß er das? Hier wird es schwierig. Er kann es wissen, indem er einen neuen Stackframe anlegt analog zum Prozedurbeginn für diesen Block, d. h. er merkt sich zu Beginn den alten Framepointer und legt die blockrelativen Variablen wie gehabt relativ mit einem negativen Offset ab. Dumm nur, daß er so nicht direkt auf die alten Variablen außerhalb des Blocks zugreifen kann. Dafür muß er sich merken, in welcher Blocktiefe er sich befindet und bei Zugriffen auf Variablen außerhalb des Blocks die Stackframes zurückverfolgen, um den Framepointer zu erhalten, der für diese Variablen paßt. (vgl. https://en.wikipedia.org/wiki/Call_stack#STACK-FRAME den Punkt "lexikally nested routines"). Das Gleiche trifft übrigens bisher in Deiner Sprache auch zu, wenn man auf globale (d. h. Hauptprogramm-lokale) Variablen zugreift.
    Bei dynamischen Typen wird die Sache dann noch viel komplizierter, da man aufgrund der sich ändernden Größe der Variablen nicht weiß, wo die Variablen sich relativ zum Framepointer befinden. Daher darf man auch nicht direkt in die Bezeichnerliste den Offset eintragen, da sich dieser bei jedem Aufruf (auch rekursiv) ändert. Stattdessen legt man in der Bezeichnerliste einen relativen Zeiger auf einen Zeiger auf den Stack ab. Für jeden Zugriff auf eine Variable muß also dieser Prozeß durchlaufen werden: 1.) Schlage die Variable in der Bezeichnerliste nach. 2.) Hole von dort den relativen Offset auf den Stack. 3.) Lade von der Adresse (Framepointer + relativer Zeiger) den eigentlichen Zeiger. 4) Adressiere den Speicher über diesen Zeiger. Und das auf dem 6502...
    Tut mir leid, besser als dies kann ich es in der Kürze hier im Forum nicht erklären. Solltest Du das Problem immer noch nicht erkennen, bitte ich Dich, zunächst die genannten Quellen zu studieren und vielleicht ein Buch zur Compilerprogrammierung heranzuziehen, in dem noch mal ausführlicher auf das Thema Stackframe eingegangen wird.

  • Du siehst ein Mißverständnis, das keines ist.

    Einzige Lösung: Zu Beginn der Prozedur legt man einen zweiten Stapelzeiger fest, den frame pointer.

    Es gibt einen Frame Pointer während des Ablaufs, der sich mit dem Funktionsaufruf erhöht (oder erniedrigt, wohin der Stack eben wächst), und statische Offsets für die lokalen Variablen etc. Damit ist zu jeder Zeit in jeder Prozedur die Addresse jeder lokalen Variable als Frame-Pointer+Offset eindeutig bestimmt, und das braucht eine indirekt-indizierte Addressierung.


    Aber dieses Verfahren benötigt zwingend 2 Zeiger: Einen auf den aktuellen Stapel (SP), einen als Basisadresse auf die Variable (Framepointer).

    Einen dynamischen (SP) zur Laufzeit (wobei die Basis des Stacks auch irgendwo bekannt sein muss, aber das lasse ich jetzt weg) und einen statischen Offset, der im Zugriff bereits codiert ist, da die relative Position der Variable statisch bekannt ist.


    Während des Programmablaufs mußt Du weiterhin den Stapelzeiger mitberechnen, da Dein Programm nicht vollständig kompiliert wird, Du also nicht weißt, wo sich Deine Variablen relativ zum Framepointer befinden.

    Ich gehe natürlich davon aus, dass dieser Offset zur Compile-Zeit berechnet wird. Wenn du meinst, ich hätte das nicht so vorgehabt, verstehe ich das Mißverständnis. Also doch, das habe ich ganz entschieden so vor.


    Solltest Du das Problem immer noch nicht erkennen

    Doch, es handelt sich um einen Kommunikationsfehler zwischen uns - den ich gerne auf meine Kappe nehme, denn Information ist bekanntlich das, was ankommt. ;-)


    Nachtrag:

    Hier erweist sich deine Bemerkung, dass man Daten- und Evaluations-Stack normalerweise trennt, als sehr wertvoll! Alles andere wär in dem Fall wirklich grober Unfug! Der Daten-Stack ist so wirklich gut aufgeräumt.