Wie definieren und speichern Programmiersprachen Funktionen / Methoden? Ich erstelle eine interpretierte Programmiersprache in Ruby und versuche herauszufinden, wie die Funktionsdeklaration implementiert wird.
Meine erste Idee ist, den Inhalt der Deklaration in einer Karte zu speichern. Zum Beispiel, wenn ich so etwas gemacht habe
def a() {
callSomething();
x += 5;
}
Dann würde ich einen Eintrag in meine Karte einfügen:
{
'a' => 'callSomething(); x += 5;'
}
Das Problem dabei ist, dass es rekursiv wird, weil ich meine parse
Methode für die Zeichenfolge aufrufen muss, die dann parse
erneut aufgerufen wird, wenn sie auftritt doSomething
, und schließlich nicht mehr genügend Stapelspeicherplatz zur Verfügung steht.
Wie gehen interpretierte Sprachen damit um?
Antworten:
Wäre es richtig anzunehmen, dass Ihre "Analyse" -Funktion den Code nicht nur analysiert, sondern gleichzeitig ausführt? Wenn Sie dies auf diese Weise tun möchten, speichern Sie den Ort der Funktion , anstatt den Inhalt einer Funktion in Ihrer Map zu speichern .
Aber es gibt einen besseren Weg. Der Aufwand im Vorfeld ist etwas höher, aber mit zunehmender Komplexität lassen sich viel bessere Ergebnisse erzielen: Verwenden Sie einen abstrakten Syntaxbaum.
Die Grundidee ist, dass Sie den Code immer nur einmal analysieren. Dann haben Sie eine Reihe von Datentypen, die Operationen und Werte darstellen, und Sie erstellen daraus einen Baum wie folgt:
wird:
(Dies ist nur eine Textdarstellung der Struktur eines hypothetischen AST. Der tatsächliche Baum wäre wahrscheinlich nicht in Textform.) Wie auch immer, Sie analysieren Ihren Code in einen AST und führen dann entweder Ihren Interpreter direkt über den AST aus. Oder verwenden Sie einen zweiten Durchgang ("Codegenerierung"), um den AST in eine Ausgabeform zu verwandeln.
Im Falle Ihrer Sprache würden Sie wahrscheinlich eine Zuordnung haben, die Funktionsnamen auf Funktions-ASTs abbildet, anstatt Funktionsnamen auf Funktionszeichenfolgen.
quelle
(((((((((((((((( x )))))))))))))))))
. In Wirklichkeit können Stapel viel größer sein, und die grammatikalische Komplexität von echtem Code ist sehr begrenzt. Sicher, wenn dieser Code für Menschen lesbar sein muss.Sie sollten beim Sehen nicht parsen
callSomething()
(ich nehme an, Sie meinten escallSomething
eher alsdoSomething
). Der Unterschied zwischena
undcallSomething
besteht darin, dass eines eine Methodendefinition ist, während das andere ein Methodenaufruf ist.Wenn Sie eine neue Definition sehen, möchten Sie überprüfen, ob Sie diese Definition hinzufügen können.
Angenommen, diese Prüfungen bestehen, können Sie sie Ihrer Karte hinzufügen und den Inhalt dieser Methode überprüfen.
Wenn Sie einen Methodenaufruf wie finden
callSomething()
, sollten Sie die folgenden Überprüfungen durchführen:callSomething
in Ihrer Karte?Wenn Sie der
callSomething()
Meinung sind , dass dies in Ordnung ist, hängt das, was Sie zu diesem Zeitpunkt tun möchten, wirklich davon ab, wie Sie es angehen möchten. Streng genommen können Sie, sobald Sie wissen, dass ein solcher Aufruf zu diesem Zeitpunkt in Ordnung ist, nur den Namen der Methode und die Argumente speichern, ohne auf weitere Details einzugehen. Wenn Sie Ihr Programm ausführen, rufen Sie die Methode mit den Argumenten auf, die Sie zur Laufzeit haben sollten.Wenn Sie weiter gehen möchten, können Sie nicht nur die Zeichenfolge, sondern auch einen Link zur eigentlichen Methode speichern. Dies wäre effizienter, aber wenn Sie den Speicher verwalten müssen, kann dies verwirrend werden. Ich würde empfehlen, zuerst einfach an der Saite festzuhalten. Später können Sie versuchen, zu optimieren.
Beachten Sie, dass dies alles vorausgesetzt wird, dass Sie Ihr Programm lexxiert haben, was bedeutet, dass Sie alle Token in Ihrem Programm erkannt haben und wissen, was sie sind . Das heißt nicht, dass Sie wissen, ob sie zusammen noch einen Sinn ergeben. Dies ist die Analysephase. Wenn Sie die Token noch nicht kennen, sollten Sie sich zunächst darauf konzentrieren, diese Informationen zu erhalten.
Ich hoffe das hilft! Willkommen bei Programmers SE!
quelle
Als ich Ihren Beitrag las, bemerkte ich zwei Fragen in Ihrer Frage. Das wichtigste ist, wie man parst. Es gibt viele Arten von Parsern (z. B. Parser für rekursive Abstammung , LR-Parser , Packrat-Parser ) und Parser-Generatoren (z. B. GNU Bison , ANTLR ), mit denen Sie ein Textprogramm "rekursiv" mit einer (expliziten oder impliziten) Grammatik durchlaufen können.
Die zweite Frage betrifft das Speicherformat für Funktionen. Wenn Sie keine syntaxgesteuerte Übersetzung durchführen , erstellen Sie eine Zwischendarstellung Ihres Programms, bei der es sich um einen abstrakten Syntaxbaum oder eine benutzerdefinierte Zwischensprache handeln kann, um die weitere Verarbeitung zu ermöglichen (Kompilieren, Transformieren, Ausführen, Beschreiben) eine Datei, etc).
quelle
Aus allgemeiner Sicht ist die Definition einer Funktion kaum mehr als eine Beschriftung oder ein Lesezeichen im Code. Die meisten anderen Schleifen-, Bereichs- und Bedingungsoperatoren sind ähnlich. Sie stehen für einen grundlegenden "Sprung" - oder "Sprung" -Befehl in den unteren Abstraktionsebenen. Ein Funktionsaufruf besteht im Wesentlichen aus den folgenden einfachen Computerbefehlen:
Eine "return" -Anweisung oder ähnliches bewirkt dann Folgendes:
Funktionen sind daher einfach Abstraktionen in einer übergeordneten Sprachspezifikation, die es dem Menschen ermöglichen, Code auf eine wartbarere und intuitivere Weise zu organisieren. Beim Kompilieren in eine Assembly- oder Zwischensprache (JIL, MSIL, ILX) und definitiv beim Rendern als Maschinencode gehen fast alle derartigen Abstraktionen verloren.
quelle