
Einführung
Grüße an alle, die vorbeigekommen sind, um meinen nächsten Artikel zu lesen.
Ich wiederhole, ich beschreibe die Erstellung einer Programmiersprachensprache basierend auf früheren Arbeiten, deren Ergebnisse
in diesem Beitrag beschrieben werden .
Im ersten Teil (Link:
habr.com/post/435202 ) habe ich die Phasen des Entwerfens und Schreibens einer Sprach-VM beschrieben, die unsere zukünftigen Anwendungen in unserer zukünftigen Sprache ausführen wird.
In diesem Artikel möchte ich die Hauptschritte beim Erstellen einer Zwischenprogrammiersprache beschreiben, die zu einem abstrakten Bytecode für die direkte Ausführung auf unserer VM zusammengesetzt wird.
Ich denke, dass es nicht schaden wird, sofort Links zur Projektwebsite und ihrem Repository bereitzustellen.
WebsiteRepositoryIch muss sofort sagen, dass der gesamte Code in FPC geschrieben ist und ich werde Beispiele dafür geben.
Also beginnen wir unsere Erleuchtung.
Warum haben wir die Zwischensprache aufgegeben?
Es lohnt sich zu verstehen, dass die Konvertierung eines Programms von einer Hochsprache direkt in einen ausführbaren Bytecode, der aus einem begrenzten Satz von Anweisungen besteht, so trivial ist, dass es besser ist, es um eine Größenordnung zu vereinfachen, indem dem Projekt eine Zwischensprache hinzugefügt wird. Es ist viel besser, den Code schrittweise zu vereinfachen, als mathematische Ausdrücke, Strukturen und Klassen sofort mit einer Reihe von Opcodes darzustellen. Dies ist übrigens die Art und Weise, wie die meisten Übersetzer und Compiler von Drittanbietern arbeiten.
In meinem vorherigen Artikel habe ich darüber geschrieben, wie eine Sprach-VM implementiert wird. Jetzt müssen wir eine Assembler-ähnliche Sprache und Funktionen zum weiteren Schreiben des Übersetzers implementieren. In diesen Phasen legen wir den Grundstein für das zukünftige Projekt. Es lohnt sich zu verstehen, dass das Gebäude umso steiler ist, je besser das Fundament ist.
Wir machen den ersten Schritt, um dieses Wunder zu verwirklichen
Zunächst lohnt es sich, ein Ziel zu setzen. Was werden wir eigentlich schreiben? Welche Eigenschaften sollte der endgültige Code haben und was sollte er tun?
Ich kann eine Liste der Hauptfunktionsteile erstellen, aus denen dieser Teil des Projekts bestehen sollte:
- Einfacher Assembler. Konvertiert einfache Anweisungen in eine Reihe von Opcodes für VMs.
- Die grundlegende Implementierung der Funktion zum Implementieren von Variablen.
- Die grundlegende Implementierung der Funktion zum Arbeiten mit Konstanten.
- Funktionalität zur Unterstützung von Einstiegspunkten in Methoden und zur Berechnung ihrer Adressen in der Übersetzungsphase.
- Vielleicht noch ein paar funktionelle Brötchen.
Die obige Abbildung zeigt ein Codefragment in einer Zwischensprache, das von einem primitiven Übersetzer in Code für eine VM konvertiert wird. Dies wird erläutert.
Nachdem die Ziele festgelegt wurden, fahren wir mit der Implementierung fort.
Einen einfachen Assembler schreiben
Wir fragen uns, was ist Assembler?
Tatsächlich ist dies ein Programm, das die Ersetzung von Opcodes anstelle ihrer Textbeschreibungen durchführt.
Betrachten Sie diesen Code:
push 0 push 1 add peek 2 pop
Nach der Verarbeitung des Assembler-Codes erhalten wir den ausführbaren Code für die VM.
Wir sehen, dass die Anweisungen einsilbig und bisilbig sein können. Keine komplizierten Anweisungen mehr für die gestapelte VM.
Wir benötigen einen Code, der Token aus einer Zeichenfolge extrahieren kann (wir berücksichtigen, dass sich möglicherweise Zeichenfolgen unter ihnen befinden).
Wir schreiben es:
function Tk(s: string; w: word): string; begin Result := ''; while (length(s) > 0) and (w > 0) do begin if s[1] = '"' then begin Delete(s, 1, 1); Result := copy(s, 1, pos('"', s) - 1); Delete(s, 1, pos('"', s)); s := trim(s); end else if Pos(' ', s) > 0 then begin Result := copy(s, 1, pos(' ', s) - 1); Delete(s, 1, pos(' ', s)); s := trim(s); end else begin Result := s; s := ''; end; Dec(w); end; end;
Ok, jetzt müssen wir für jede Anweisung so etwas wie ein Switch-Case-Konstrukt implementieren, und unser einfacher Assembler ist bereit.
Variablen
Denken Sie daran, dass unsere VM über ein Array von Zeigern verfügt, die Variablen und dementsprechend statische Adressierung unterstützen. Dies bedeutet, dass die Funktion zum Arbeiten mit Variablen als TStringList dargestellt werden kann, in der Zeichenfolgen die Namen von Variablen und ihre Indizes ihre statischen Adressen sind. Es versteht sich, dass das Duplizieren von Variablennamen in dieser Liste nicht akzeptabel ist. Ich denke, Sie können sich den notwendigen Code vorstellen und / oder ihn sogar selbst schreiben.
Wenn Sie sich die fertige Implementierung ansehen möchten, sind Sie herzlich willkommen: /lang/u_variables.pas
Konstanten
Das Prinzip ist hier dasselbe wie bei Variablen, aber es gibt eine Sache. Zur Optimierung ist es besser, nicht an die Namen von Konstanten, sondern an deren Werte zu binden. Das heißt, Jeder konstante Wert kann eine TStringList haben, in der die Namen von Konstanten mit diesem Wert gespeichert werden.
Für Konstanten sollten Sie den Datentyp angeben. Um sie der Sprache hinzuzufügen, müssen Sie einen kleinen Parser schreiben.
Implementierung: /lang/u_consts.pas
Methodeneintrittspunkte
Um Codeblockierung, Unterstützung für verschiedene Designs usw. zu implementieren. Die Unterstützung für diese Funktionalität sollte auf Assembler-Ebene implementiert werden.
Betrachten Sie ein Codebeispiel:
Summ: peek 0 pop peek 1 pop push 0 new peek 2 mov push 2 push 0 add jr
Das Obige ist eine Beispielübersetzung der Summ-Methode:
func Summ(a, b): return a + b end
Es versteht sich, dass es keine Opcodes für Einstiegspunkte gibt. Was ist ein Einstiegspunkt in die Summ-Methode? Diese Primzahl ist der Versatz des nächsten Opcode-Einstiegspunkts. (Der Offset des Opcodes ist die Nummer des Opcodes relativ zum Anfang des ausführbaren abstrakten Bytecodes.) Jetzt haben wir eine Aufgabe - wir müssen diesen Offset in der Kompilierungsphase berechnen und optional die Summ-Konstante als diese Zahl deklarieren.
Wir schreiben dafür für jeden Bediener einen bestimmten Gewichtszähler. Wir haben einfache einsilbige Operatoren, zum Beispiel "Pop". Sie belegen 1 Byte. Es gibt komplexere, zum Beispiel "Push 123" - sie belegen 5 Bytes, 1 für den Opcode und 4 für den vorzeichenlosen Int-Typ.
Das Wesentliche des Codes zum Hinzufügen von Unterstützung für Assembler für Einstiegspunkte:
- Wir haben einen Zähler, sagen wir i = 0.
- Wir durchlaufen den Code, wenn wir eine Konstruktion vom Typ "Push 123" haben, fügen wir 5 hinzu, wenn der einfache Opcode 1 ist. Wenn wir einen Einstiegspunkt haben, entfernen Sie ihn aus dem Code und deklarieren Sie die entsprechende Konstante mit dem Zählerwert und dem Namen des Einstiegspunkts.
Andere Funktionen
Dies ist beispielsweise eine einfache Codekonvertierung vor der Verarbeitung.
Zusammenfassung
Wir haben unseren kleinen Assembler implementiert. Wir werden es brauchen, um einen komplexeren Übersetzer basierend darauf zu implementieren. Jetzt können wir kleine Programme für unsere VM schreiben. Dementsprechend wird in weiteren Artikeln der Prozess des Schreibens eines komplexeren Übersetzers beschrieben.
Vielen Dank, dass Sie bis zum Ende gelesen haben.
Wenn Ihnen etwas nicht klar ist, warte ich auf Ihre Kommentare.