
Einführung
Ich begrüße Sie, interessierte Leseentwickler in allen Sprachen, in denen ich diese Artikel orientiere und deren Unterstützung und Meinungen ich schätze.
Für den Anfang werde ich nach etablierten Traditionen Links zu früheren Artikeln bereitstellen:
Teil 1: Schreiben einer Sprach-VMTeil 2: Zwischenpräsentation von ProgrammenUm in Ihrem Kopf ein vollständiges Verständnis dessen zu entwickeln, was wir in diesen Artikeln schreiben, sollten Sie sich im Voraus mit den vorherigen Teilen vertraut machen.
Außerdem sollte ich sofort einen Link zu einem Artikel über ein Projekt veröffentlichen, das ich zuvor geschrieben habe und auf dem diese ganze Nachbesprechung basiert:
Clack Syudy . Vielleicht lohnt es sich, sich als erstes damit vertraut zu machen.
Und ein wenig zum Projekt:
→
Kleine Projektstelle→
GitHub-RepositoryNun, ich werde auch gleich sagen, dass alles in Object Pascal geschrieben ist, nämlich in FPC.
Also fangen wir an.
Das Funktionsprinzip der meisten Übersetzer
Zuallererst ist es verständlich, dass ich nichts Wertvolles schreiben konnte, ohne mich zuerst mit einer Reihe theoretischen Materials und einer Reihe von Statuen vertraut zu machen. Ich werde die Hauptsache in ein paar Worten beschreiben.
Die Aufgabe des Übersetzers besteht zunächst darin, den Code für die Analyse vorzubereiten (z. B. Kommentare daraus zu entfernen) und den Code in Token aufzuteilen (ein Token ist der für die Sprache sinnvolle Mindestzeichensatz).
Als nächstes müssen Sie durch Analysieren und Transformieren den Code in eine bestimmte Zwischendarstellung analysieren und dann die Anwendung zusammenstellen, die zur Ausführung bereit ist, oder ... Was muss sie im Allgemeinen sammeln?
Ja, ich habe mit diesem Textbündel jedoch nichts gesagt - jetzt ist die Aufgabe in mehrere Unteraufgaben unterteilt.
Lassen Sie uns überspringen, wie der Code für die Ausführung vorbereitet wird, weil Es ist zu langweilig, einen Prozess zu beschreiben. Angenommen, wir haben eine Reihe von Token zur Analyse bereit.
Code-Analyse
Möglicherweise haben Sie von der Erstellung eines Codebaums und seiner Analyse oder von noch abstruseren Dingen gehört. Wie immer - das ist nichts weiter als die einfachen schrecklichen Begriffe durcheinander zu bringen. Mit Code-Analyse meine ich eine viel einfachere Reihe von Aktionen. Die Aufgabe besteht darin, die Liste der Token zu durchsuchen und den Code für jede seiner Konstruktionen zu analysieren.
In imperativen Sprachen wird der Code in der Regel bereits in Form einer Art Baum aus Strukturen dargestellt.
Sie müssen zugeben, dass es nicht akzeptabel ist, den Zyklus „A“ im Körper des Zyklus „B“ zu beginnen und außerhalb des Körpers des Zyklus „B“ zu beenden. Ein Code ist eine Struktur, die aus einer Reihe von Konstrukten besteht.
Und was hat jedes Design? Das ist richtig - der Anfang und das Ende (und vielleicht etwas anderes in der Mitte, aber nicht der Punkt).
Dementsprechend kann die Code-Analyse in einem Durchgang durchgeführt werden, ohne einen Baum zu erstellen.
Dazu benötigen Sie eine Schleife, die den Code durchläuft, und einen großen Switch-Case, der die Hauptcode-Analyse und -Analyse durchführt.
Das heißt, wir laufen durch die Token, wir haben ein Token (zum Beispiel, lass es sein ...) "if" - ich bezweifle wirklich, dass ein solches Token einfach so im Code sein kann -> dies ist der Beginn der if..then [.. else] .. Endkonstruktion!
Wir analysieren jeweils alle nachfolgenden Token auf die Konstruktion von Bedingungen in unserer Sprache.
Ein bisschen über Codefehler
In der Phase der Analyse von Strukturen und der Ausführung entlang des Codes ist es besser, die Fehlerverarbeitung nicht zu bewerten. Dies ist eine nützliche Übersetzerfunktion. Wenn während der Analyse der Struktur ein Fehler auftritt, ist dies logisch - die Struktur ist nicht ordnungsgemäß erstellt und Sie sollten den Entwickler benachrichtigen.
Nun zu Mash. Wie wird die Sprache analysiert?
Oben habe ich ein verallgemeinertes Konzept eines Übersetzers beschrieben. Jetzt ist die Zeit, über meine Arbeit zu sprechen.
Tatsächlich stellte sich heraus, dass der Übersetzer dem oben beschriebenen sehr ähnlich war. Aber für mich zerlegt es den Code nicht in eine Reihe von Token für die weitere Analyse.
Bevor Sie mit dem Parsen beginnen, wird der Code in einer schöneren Form dargestellt. Kommentare werden gelöscht und alle Konstruktionen werden zu langen Zeilen zusammengefasst, wenn sie in mehreren Zeilen beschrieben werden.
Somit gibt es in jeder separat genommenen Zeile eine Sprachkonstruktion oder ihren Teil. Das ist cool, jetzt können wir jede Zeile in unserem großen Switch-Case analysieren, anstatt nach diesen Konstrukten im Token-Set zu suchen. Der Vorteil hierbei ist auch, dass die Linie ein Ende hat und es mit diesem Ansatz einfacher ist, Fehler in der Konstruktion zu bestimmen.
Dementsprechend erfolgt die Analyse einzelner Strukturen nach getrennten Methoden, die eine Zwischendarstellung des Code von Strukturen oder seiner Teile zurückgeben.
P.S. In einem früheren Artikel habe ich den Aufbau eines Übersetzers von einer Zwischensprache zu einem Bytecode für eine VM beschrieben. Eigentlich - diese Zwischensprache ist eine Zwischendarstellung.
Es versteht sich, dass Strukturen aus mehreren einfacheren Strukturen bestehen können. Weil Da wir jede Struktur mit separaten Methoden analysieren, können wir sie bei der Analyse jeder Struktur leicht voneinander abrufen.

Aufwärmlauf mit dem Code
Zunächst sollte sich der Übersetzer schnell mit dem Code vertraut machen, ihn durchgehen und auf einige Entwürfe achten.
Zu diesem Zeitpunkt können Sie sich mit globalen Variablen befassen, Konstrukte verwenden sowie Importe, Prozeduren und Funktionen sowie OOP-Konstrukte verwenden.
Es ist besser, eine Zwischenansicht in mehrere Objekte zur Speicherung zu generieren, damit
Fügen Sie den Code für globale Variablen nach der Initialisierung, jedoch vor dem Start von main () ein.
Code für OOP-Konstrukte kann am Ende eingefügt werden.
Anspruchsvolle Designs
Ok, wir haben die einfachen Designs herausgefunden. Jetzt ist es Zeit für das Knifflige. Ich glaube nicht, dass Sie es geschafft haben, das Beispiel mit zwei Zyklen zu vergessen. Wie wir wissen, kommen Strukturen normalerweise in Form einer Art Baum vor. Dies bedeutet, dass wir komplexe Strukturen mithilfe des Stapels analysieren können.
Was hat der Stack damit zu tun? Darüber hinaus.
Zuerst beschreiben wir die Klasse, die wir auf den Stapel schieben werden. Wenn wir komplexe Konstruktionen analysieren, können wir eine Zwischendarstellung für den Anfang und das Ende dieses Blocks generieren. Beispielsweise analysieren wir die for-while- bis till-Schleife, wenn Konstrukte, Methoden und tatsächlich alles in der Mash-Sprache.
Diese Klasse benötigt Felder zum Speichern von Zwischendarstellungen, Metainformationen (für einige Variablenkonstruktionen) und natürlich zum Speichern eines Blocktyps.
Ich gebe nur den gesamten Code, weil hier nicht viel ist:
unit u_prep_codeblock; {$mode objfpc}{$H+} interface uses Classes, SysUtils; type TBlockEntryType = (btProc, btFunc, btIf, btFor, btWhile, btUntil, btTry, btClass, btSwitch, btCase); TCodeBlock = class(TObject) public bType: TBlockEntryType; mName, bMeta, bMCode, bEndCode: string; constructor Create(bt: TBlockEntryType; MT, MC, EC: string); end; implementation constructor TCodeBlock.Create(bt: TBlockEntryType; MT, MC, EC: string); begin inherited Create; bType := bt; bMeta := MT; bMCode := MC; bEndCode := EC; end; end.
Nun, der Stack ist eine einfache TList, das Rad hier neu zu erfinden ist einfach nur dumm.
Nehmen wir also beim Parsen der Konstruktion an, dass die while-Schleife wie folgt aussieht:
function ParseWhile(s: string; varmgr: TVarManager): string; var WhileNum, ExprCode: string; begin Delete(s, 1, 5);
Über mathematische Ausdrücke
Sie haben dies vielleicht nicht bemerkt, aber mathematische / logische Ausdrücke sind auch strukturierter Code.
Ich habe ihre Analyse gestapelt implementiert. Zuerst werden alle einzelnen Elemente des Ausdrucks auf den Stapel geschoben, dann wird in mehreren Durchgängen der Code für die Zwischendarstellung erzeugt.
Mehrmals - weil Es gibt vorrangige mathematische Operationen wie die Multiplikation.
Ich verstehe den Punkt hier nicht, weil es ist viel davon und es ist langweilig.
P.S. /lang/u_prep_expressions.pas - hier wird es für Ihre Überprüfung vollständig und vollständig angezeigt.
Zusammenfassung
Also haben wir einen Übersetzer implementiert, der konvertieren kann ... Zum Beispiel ist dies der Code:
proc PrintArr(arr): for(i ?= 0; i < len(arr); i++): PrintLn("arr[", i, "] = ", arr[i]) end end proc main(): var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] PrintArr(arr) InputLn() end
Was fehlt in unserer Sprache? Richtig, unterstütze OOP. Wir werden darüber in meinem nächsten Artikel sprechen.
Vielen Dank, dass Sie bis zum Ende gelesen haben.
Wenn Ihnen etwas nicht klar ist, warte ich auf Ihre Kommentare. Oder Fragen im
Forum , ja ... Ja, ich überprüfe es manchmal.
Und jetzt eine kleine Umfrage (damit ich sie mir anschaue und die Bedeutung meiner Artikel genieße):