Intuitiv scheint es, dass ein Compiler für Sprache Foo
nicht selbst in Foo geschrieben werden kann. Insbesondere kann der erste Compiler für Sprache Foo
nicht in Foo geschrieben werden, aber jeder nachfolgende Compiler könnte für geschrieben werden Foo
.
Aber ist das tatsächlich wahr? Ich habe eine sehr vage Erinnerung daran, wie ich über eine Sprache gelesen habe, deren erster Compiler in "sich selbst" geschrieben wurde. Ist das möglich und wenn ja wie?
Antworten:
Dies wird als "Bootstrapping" bezeichnet. Sie müssen zuerst einen Compiler (oder Interpreter) für Ihre Sprache in einer anderen Sprache (normalerweise Java oder C) erstellen. Sobald dies erledigt ist, können Sie eine neue Version des Compilers in der Sprache Foo schreiben. Sie verwenden den ersten Bootstrap-Compiler, um den Compiler zu kompilieren, und verwenden dann diesen kompilierten Compiler, um alles andere (einschließlich zukünftiger Versionen von sich selbst) zu kompilieren.
Die meisten Sprachen werden in der Tat auf diese Weise erstellt, teilweise weil Sprachdesigner die von ihnen erstellte Sprache gerne verwenden, und auch, weil ein nicht trivialer Compiler häufig als nützlicher Maßstab dafür dient, wie "vollständig" die Sprache sein kann.
Ein Beispiel hierfür wäre Scala. Der erste Compiler wurde in Pizza erstellt, einer experimentellen Sprache von Martin Odersky. Ab Version 2.0 wurde der Compiler in Scala komplett neu geschrieben. Von diesem Zeitpunkt an konnte der alte Pizza-Compiler vollständig verworfen werden, da der neue Scala-Compiler verwendet werden konnte, um sich für zukünftige Iterationen selbst zu kompilieren.
quelle
Ich erinnere mich an einen Podcast von Software Engineering Radio, in dem Dick Gabriel über das Bootstrapping des ursprünglichen LISP-Interpreters sprach, indem er eine Bare-Bones-Version in LISP auf Papier schrieb und sie von Hand zu Maschinencode zusammensetzte. Von da an wurden die restlichen LISP-Funktionen in LISP geschrieben und mit LISP interpretiert.
quelle
Den vorherigen Antworten eine Neugier hinzufügen.
Hier ist ein Zitat aus dem Linux From Scratch- Handbuch, in dem Schritt, in dem mit dem Erstellen des GCC-Compilers aus seiner Quelle begonnen wird. (Linux From Scratch ist eine Möglichkeit, Linux zu installieren, die sich grundlegend von der Installation einer Distribution unterscheidet, da Sie wirklich jede einzelne Binärdatei des Zielsystems kompilieren müssen .)
Diese Verwendung des 'Bootstrap'-Ziels ist durch die Tatsache motiviert, dass der Compiler, mit dem die Toolchain des Zielsystems erstellt wird, möglicherweise nicht dieselbe Version des Ziel-Compilers hat. Wenn man so vorgeht, erhält man im Zielsystem sicher einen Compiler, der sich selbst kompilieren kann.
quelle
Wenn Sie Ihren ersten Compiler für C schreiben, schreiben Sie ihn in einer anderen Sprache. Jetzt haben Sie einen Compiler für C in Assembler. Schließlich kommen Sie an den Ort, an dem Sie Zeichenfolgen analysieren müssen, insbesondere Escape-Sequenzen. Sie schreiben Code, um ihn
\n
in das Zeichen mit dem Dezimalcode 10 (und\r
in 13 usw.) umzuwandeln .Nachdem dieser Compiler fertig ist, werden Sie ihn in C erneut implementieren. Dieser Vorgang wird als " Bootstrapping " bezeichnet.
Der String-Parsing-Code lautet:
Wenn dies kompiliert wird, haben Sie eine Binärdatei, die '\ n' versteht. Dies bedeutet, dass Sie den Quellcode ändern können:
Wo ist also die Information, dass '\ n' der Code für 13 ist? Es ist in der Binärdatei! Es ist wie bei DNA: Das Kompilieren von C-Quellcode mit dieser Binärdatei erbt diese Informationen. Wenn der Compiler sich selbst kompiliert, gibt er dieses Wissen an seine Nachkommen weiter. Ab diesem Zeitpunkt ist es nicht mehr möglich, allein aus der Quelle zu erkennen, was der Compiler tun wird.
Wenn Sie einen Virus in der Quelle eines Programms verstecken möchten, gehen Sie folgendermaßen vor: Rufen Sie die Quelle eines Compilers ab, suchen Sie die Funktion, die Funktionen kompiliert, und ersetzen Sie sie durch diese:
Die interessanten Teile sind A und B. A ist der Quellcode für
compileFunction
die Aufnahme des Virus, wahrscheinlich auf irgendeine Weise verschlüsselt, sodass es bei der Suche in der resultierenden Binärdatei nicht offensichtlich ist. Dadurch wird sichergestellt, dass beim Kompilieren in den Compiler mit sich selbst der Virusinjektionscode erhalten bleibt.B ist dasselbe für die Funktion, die wir durch unseren Virus ersetzen möchten. Zum Beispiel könnte es die Funktion "login" in der Quelldatei "login.c" sein, die wahrscheinlich vom Linux-Kernel stammt. Wir könnten es durch eine Version ersetzen, die zusätzlich zum normalen Passwort das Passwort "joshua" für das Root-Konto akzeptiert.
Wenn Sie das kompilieren und als Binärdatei verbreiten, können Sie den Virus nicht anhand der Quelle finden.
Die ursprüngliche Quelle der Idee: https://web.archive.org/web/20070714062657/http://www.acm.org/classics/sep95/
quelle
Sie können keinen Compiler selbst schreiben, da Sie nichts haben, mit dem Sie Ihren Startquellcode kompilieren können. Es gibt zwei Lösungsansätze.
Am wenigsten bevorzugt ist das Folgende. Sie schreiben einen minimalen Compiler in Assembler (yuck) für einen minimalen Satz der Sprache und verwenden diesen Compiler dann, um zusätzliche Funktionen der Sprache zu implementieren. Bauen Sie sich auf, bis Sie einen Compiler mit allen Sprachfunktionen für sich haben. Ein schmerzhafter Prozess, der normalerweise nur durchgeführt wird, wenn Sie keine andere Wahl haben.
Der bevorzugte Ansatz ist die Verwendung eines Cross-Compilers. Sie ändern das Back-End eines vorhandenen Compilers auf einem anderen Computer, um eine Ausgabe zu erstellen, die auf dem Zielcomputer ausgeführt wird. Dann haben Sie einen schönen vollständigen Compiler, der auf dem Zielcomputer arbeitet. Am beliebtesten ist hierfür die C-Sprache, da es viele vorhandene Compiler gibt, die über steckbare Backends verfügen, die ausgetauscht werden können.
Eine wenig bekannte Tatsache ist, dass der GNU C ++ - Compiler eine Implementierung hat, die nur die C-Teilmenge verwendet. Der Grund dafür ist normalerweise, dass es einfach ist, einen C-Compiler für einen neuen Zielcomputer zu finden, mit dem Sie den vollständigen GNU C ++ - Compiler daraus erstellen können. Sie haben sich jetzt darauf festgelegt, einen C ++ - Compiler auf dem Zielcomputer zu haben.
quelle
Im Allgemeinen muss zuerst ein funktionierender (wenn auch primitiver) Schnitt des Compilers funktionieren - dann können Sie darüber nachdenken, ihn selbst zu hosten. Dies wird in einigen Sprachen tatsächlich als wichtiger Meilenstein angesehen.
Soweit ich mich an "Mono" erinnere, müssen sie wahrscheinlich ein paar Dinge zur Reflexion hinzufügen, damit es funktioniert: Das Mono-Team weist immer wieder darauf hin, dass einige Dinge mit einfach nicht möglich sind
Reflection.Emit
; Natürlich könnte das MS-Team ihnen das Gegenteil beweisen.Dies hat einige echte Vorteile: Für den Anfang ist es ein ziemlich guter Unit-Test! Und Sie müssen sich nur um eine Sprache kümmern (dh es ist möglich, dass ein C # -Experte nicht viel über C ++ weiß; jetzt können Sie den C # -Compiler reparieren). Aber ich frage mich, ob hier nicht viel professioneller Stolz am Werk ist: Sie wollen einfach, dass es sich selbst hostet.
Nicht ganz ein Compiler, aber ich habe kürzlich an einem System gearbeitet, das sich selbst hostet. Der Codegenerator wird verwendet, um den Codegenerator zu generieren. Wenn sich das Schema ändert, führe ich es einfach auf sich selbst aus: neue Version. Wenn es einen Fehler gibt, gehe ich einfach zu einer früheren Version zurück und versuche es erneut. Sehr praktisch und sehr pflegeleicht.
Update 1
Ich habe gerade dieses Video von Anders bei PDC gesehen und (ungefähr eine Stunde später) gibt er einige viel gültigere Gründe an - alles über den Compiler als Service. Nur für das Protokoll.
quelle
Hier ist ein Dump (eigentlich schwer zu suchendes Thema):
Smalltalk
C.
Dies ist auch die Idee von PyPy und Rubinius :
(Ich denke, das könnte auch für Forth gelten , aber ich weiß nichts über Forth.)
quelle
Für GNAT, den GNU Ada-Compiler, muss ein Ada-Compiler vollständig erstellt sein. Dies kann schmerzhaft sein, wenn Sie es auf eine Plattform portieren, auf der keine GNAT-Binärdatei verfügbar ist.
quelle
Tatsächlich sind die meisten Compiler aus den oben genannten Gründen in der Sprache geschrieben, die sie kompilieren.
Der erste Bootstrap-Compiler ist normalerweise in C, C ++ oder Assembly geschrieben.
quelle
Der C # -Compiler des Mono-Projekts ist seit langem "selbst gehostet". Dies bedeutet, dass er in C # selbst geschrieben wurde.
Was ich weiß ist, dass der Compiler als reiner C-Code gestartet wurde, aber sobald die "grundlegenden" Funktionen von ECMA implementiert wurden, haben sie begonnen, den Compiler in C # neu zu schreiben.
Ich bin mir der Vorteile des Schreibens des Compilers in derselben Sprache nicht bewusst, aber ich bin sicher, dass dies zumindest mit den Funktionen zu tun hat, die die Sprache selbst bieten kann (C unterstützt beispielsweise keine objektorientierte Programmierung). .
Weitere Informationen finden Sie hier .
quelle
Ich habe SLIC (System of Languages for Implementing Compilers) in sich selbst geschrieben. Dann von Hand zusammengestellt. SLIC hat viel zu bieten, da es sich um einen einzelnen Compiler aus fünf Untersprachen handelte:
SLIC wurde von CWIC (Compiler zum Schreiben und Implementieren von Compilern) inspiriert. Im Gegensatz zu den meisten Compiler-Entwicklungspaketen haben SLIC und CWIC die Codegenerierung mit speziellen, domänenspezifischen Sprachen behandelt. SLIC erweitert die Codegenerierung von CWICs um die Subsprachen ISO, PSEUDO und MACHOP, die die Besonderheiten des Zielcomputers von der Sprache des Generators für das Crawlen von Bäumen trennen.
LISP 2 Bäume und Listen
Das dynamische Speicherverwaltungssystem der LISP 2-basierten Generatorsprache ist eine Schlüsselkomponente. Listen werden in der Sprache in eckigen Klammern angegeben, deren Komponenten durch Kommas getrennt sind, dh eine Liste mit drei Elementen [a, b, c].
Bäume:
werden durch Listen dargestellt, deren erster Eintrag ein Knotenobjekt ist:
Bäume werden normalerweise mit dem separaten Knoten vor den Zweigen angezeigt:
Aufheben der Analyse mit LISP 2-basierten Generatorfunktionen
Eine Generatorfunktion ist eine benannte Menge von (unparse) => Aktion> Paaren ...
Unparse-Ausdrücke sind Tests, die mit Baummustern und / oder Objekttypen übereinstimmen, die diese aufteilen und diese Teile einer lokalen Variablen zuweisen, die von ihrer prozeduralen Aktion verarbeitet werden soll. Ein bisschen wie eine überladene Funktion, die verschiedene Argumenttypen verwendet. Außer die () => ... Tests werden in der angegebenen Reihenfolge versucht. Die erste erfolgreiche unparse, die ihre entsprechende Aktion ausführt. Die unparsen Ausdrücke sind Zerlegungsprüfungen. ADD [x, y] entspricht einem ADD-Baum mit zwei Zweigen, der seine Zweige den lokalen Variablen x und y zuweist. Die Aktion kann ein einfacher Ausdruck oder ein gebundener Codeblock .BEGIN ... .END sein. Ich würde heute Blöcke im c-Stil {...} verwenden. Baumabgleich, [], unparse Regeln können Generatoren aufrufen, die die zurückgegebenen Ergebnisse an die Aktion übergeben:
Insbesondere stimmt die obige unparse expr_gen mit einem ADD-Baum mit zwei Zweigen überein. Innerhalb des Testmusters wird ein einzelner Argumentgenerator, der in einem Ast platziert ist, mit diesem Zweig aufgerufen. Die Argumentliste besteht jedoch aus lokalen Variablen, denen zurückgegebene Objekte zugewiesen wurden. Über dem unparse wird angegeben, dass zwei Zweige die ADD-Baumzerlegung sind, wobei rekursiv jeder Zweig auf expr_gen gedrückt wird. Die linke Verzweigungsrückgabe wird in die lokalen Variablen x eingefügt. Ebenso wurde der rechte Zweig mit y dem Rückgabeobjekt an expr_gen übergeben. Das Obige könnte Teil eines Evaluators für numerische Ausdrücke sein. Es gab Verknüpfungsmerkmale, die als Vektoren bezeichnet wurden. Oben konnte anstelle der Knotenzeichenfolge ein Knotenvektor mit einem Vektor entsprechender Aktionen verwendet werden:
Der obige vollständigere Ausdrucksauswerter weist die Rückgabe vom linken Zweig expr_gen an x und den rechten Zweig an y zu. Der entsprechende Aktionsvektor, der für x und y ausgeführt wurde, wurde zurückgegeben. Die letzten unparse => Aktionspaare stimmen mit numerischen und Symbolobjekten überein.
Symbol und Symbolattribute
Symbole können benannte Attribute haben. val: (x) Zugriff auf das val-Attribut des in x enthaltenen Symbolobjekts. Ein verallgemeinerter Symboltabellenstapel ist Teil von SLIC. Die SYMBOL-Tabelle kann verschoben und geöffnet werden, um lokale Symbole für Funktionen bereitzustellen. Neu erstellte Symbole werden in der oberen Symboltabelle katalogisiert. Die Symbolsuche durchsucht den Symboltabellenstapel von der obersten Tabelle zuerst rückwärts den Stapel hinunter.
Maschinenunabhängigen Code generieren
Die Generatorsprache von SLIC erzeugt PSEUDO-Anweisungsobjekte und hängt sie an eine Abschnittscodeliste an. Ein .FLUSH bewirkt, dass seine PSEUDO-Codeliste ausgeführt wird, wobei jeder PSEUDO-Befehl aus der Liste entfernt und aufgerufen wird. Nach der Ausführung wird ein PSEUDO-Objektspeicher freigegeben. Die Verfahrensorgane der PSEUDO- und GENERATOR-Aktionen sind bis auf ihre Ausgabe grundsätzlich dieselbe Sprache. PSEUDO sollen als Assembly-Makros fungieren und eine maschinenunabhängige Codesequenzierung ermöglichen. Sie bieten eine Trennung der spezifischen Zielmaschine von der Sprache des Baumcrawling-Generators. PSEUDOs rufen MACHOP-Funktionen auf, um Maschinencode auszugeben. MACHOPs werden verwendet, um Assembly-Pseudo-Ops (wie DC, Konstante usw.) und Maschinenanweisungen oder eine Familie von ähnlich formatierten Anweisungen unter Verwendung eines vektorisierten Eintrags zu definieren. Sie transformieren einfach ihre Parameter in eine Folge von Bitfeldern, aus denen der Befehl besteht. MACHOP-Aufrufe sollen wie Assembly aussehen und eine Druckformatierung der Felder bereitstellen, wenn Assembly in der Kompilierungsliste angezeigt wird. Im Beispielcode verwende ich Kommentare im C-Stil, die leicht hinzugefügt werden konnten, aber nicht in den Originalsprache waren. MACHOPs erzeugen Code in einem bitadressierbaren Speicher. Der SLIC-Linker verarbeitet die Ausgabe des Compilers. Ein MACHOP für die Anweisungen im DEC-10-Benutzermodus unter Verwendung eines vektorisierten Eintrags: MACHOPs erzeugen Code in einem bitadressierbaren Speicher. Der SLIC-Linker verarbeitet die Ausgabe des Compilers. Ein MACHOP für die Anweisungen im DEC-10-Benutzermodus unter Verwendung eines vektorisierten Eintrags: MACHOPs erzeugen Code in einem bitadressierbaren Speicher. Der SLIC-Linker verarbeitet die Ausgabe des Compilers. Ein MACHOP für die Anweisungen im DEC-10-Benutzermodus unter Verwendung eines vektorisierten Eintrags:
Die .MORG 36, O (18): $ / 36; Richtet die Position an einer 36-Bit-Grenze aus, wobei die Position $ / 36-Wortadresse mit 18 Bit im Oktal gedruckt wird. Das 9-Bit-Operationsregister, das 4-Bit-Register, das indirekte Bit und das 4-Bit-Indexregister werden kombiniert und gedruckt, als ob ein einzelnes 18-Bit-Feld. Die 18-Bit-Adresse / 36 oder der unmittelbare Wert wird ausgegeben und oktal gedruckt. Ein MOVEI-Beispielausdruck mit r1 = 1 und r2 = 2:
Mit der Compiler-Assembly-Option erhalten Sie den generierten Assembly-Code in der Compile-Liste.
Verknüpfe es miteinander
Der SLIC-Linker wird als Bibliothek geliefert, die die Verknüpfungs- und Symbolauflösungen verwaltet. Die zielspezifische Formatierung der Ausgabedatei muss jedoch für Zielcomputer geschrieben und mit der Linkerbibliotheksbibliothek verknüpft werden.
Die Generatorsprache kann Bäume in eine Datei schreiben und lesen, sodass ein Multipass-Compiler implementiert werden kann.
Kurze Zusammenfassung der Codegenerierung und -herkunft
Ich habe zuerst die Codegenerierung durchgesehen, um sicherzustellen, dass SLIC ein echter Compiler-Compiler ist. SLIC wurde von CWIC (Compiler for Writing and Implementing Compilers) inspiriert, das Ende der 1960er Jahre bei der Systems Development Corporation entwickelt wurde. CWIC hatte nur SYNTAX- und GENERATOR-Sprachen, die numerischen Bytecode aus der GENERATOR-Sprache erzeugten. Bytecode wurde in Speicherpuffer, die benannten Abschnitten zugeordnet sind, eingefügt oder eingefügt (der in der CWIC-Dokumentation verwendete Begriff) und durch eine .FLUSH-Anweisung ausgeschrieben. Ein ACM-Dokument zu CWIC ist im ACM-Archiv erhältlich.
Erfolgreiche Implementierung einer wichtigen Programmiersprache
In den späten 1970er Jahren wurde SLIC verwendet, um einen COBOL-Cross-Compiler zu schreiben. Fertiggestellt in ca. 3 Monaten meist von einem einzigen Programmierer. Ich habe nach Bedarf ein bisschen mit dem Programmierer gearbeitet. Ein anderer Programmierer hat die Laufzeitbibliothek und MACHOPs für den Ziel-TI-990-Mini-COMPUTER geschrieben. Dieser COBOL-Compiler hat wesentlich mehr Zeilen pro Sekunde kompiliert als der native DEC-10-COBOL-Compiler, der in Assembly geschrieben wurde.
Mehr zu einem Compiler, über den dann normalerweise gesprochen wird
Ein großer Teil des Schreibens eines Compilers von Grund auf ist die Laufzeitbibliothek. Sie benötigen eine Symboltabelle. Sie benötigen Eingabe und Ausgabe. Dynamische Speicherverwaltung usw. Das Schreiben der Laufzeitbibliothek für einen Compiler kann einfacher sein als das Schreiben des Compilers. Mit SLIC ist diese Laufzeitbibliothek jedoch allen in SLIC entwickelten Compilern gemeinsam. Beachten Sie, dass es zwei Laufzeitbibliotheken gibt. Eine für die Zielmaschine der Sprache (z. B. COBOL). Die andere ist die Laufzeitbibliothek des Compilers.
Ich glaube, ich habe festgestellt, dass dies keine Parser-Generatoren waren. Mit ein wenig Verständnis des Backends kann ich nun die Parser-Programmiersprache erklären.
Parser-Programmiersprache
Der Parser wird mit einer Formel geschrieben, die in Form einfacher Gleichungen geschrieben ist.
Das Sprachelement auf der untersten Ebene ist das Zeichen. Token werden aus einer Teilmenge der Zeichen der Sprache gebildet. Zeichenklassen werden verwendet, um diese Zeichenuntergruppen zu benennen und zu definieren. Der Operator, der die Zeichenklasse definiert, ist das Doppelpunktzeichen (:). Zeichen, die Mitglieder der Klasse sind, werden auf der rechten Seite der Definition codiert. Druckbare Zeichen sind in Primzahlen-Einzelzeichenfolgen eingeschlossen. Nicht druckbare und Sonderzeichen können durch ihre numerische Ordnungszahl dargestellt werden. Klassenmitglieder werden durch eine Alternative getrennt Operator. Eine Klassenformel endet mit einem Semikolon. Zeichenklassen können zuvor definierte Klassen enthalten:
Die skip_class 0b00000001 ist vordefiniert, kann jedoch überlastet sein, um eine skip_class zu definieren.
Zusammenfassend: Eine Zeichenklasse ist eine Liste von Alternativen, die nur eine Zeichenkonstante, eine Ordnungszahl eines Zeichens oder eine zuvor definierte Zeichenklasse sein kann. Wie ich Zeichenklassen implementiert habe: Der Klassenformel wird eine Klassenbitmaske zugewiesen. (In den obigen Kommentaren gezeigt) Jede Klassenformel mit einem Zeichenliteral oder einer Ordnungszahl bewirkt, dass ein Klassenbit zugewiesen wird. Eine Maske wird erstellt, indem die Klassenmaske (n) der enthaltenen Klasse (n) zusammen mit dem zugewiesenen Bit (falls vorhanden) geordnet werden. Aus den Zeichenklassen wird eine Klassentabelle erstellt. Ein durch die Ordnungszahl eines Charakters indizierter Eintrag enthält Bits, die die Klassenmitgliedschaften des Charakters angeben. Klassentests werden inline durchgeführt. Ein IA-86-Codebeispiel mit der Ordnungszahl des Zeichens in eax veranschaulicht das Testen von Klassen:
Gefolgt von einem:
oder
IA-86-Anweisungscodebeispiele werden verwendet, weil ich denke, dass IA-86-Anweisungen heute bekannter sind. Der Klassenname, der zu seiner Klassenmaske ausgewertet wird, ist zerstörungsfrei UND-verknüpft mit der Klassentabelle, die durch die Ordnungszahl (in eax) indiziert ist. Ein Ergebnis ungleich Null zeigt eine Klassenmitgliedschaft an. (EAX ist auf Null gesetzt, mit Ausnahme von al (den niedrigen 8 Bits von EAX), das das Zeichen enthält).
Tokens waren in diesen alten Compilern etwas anders. Schlüsselwörter wurden nicht als Token erklärt. Sie wurden einfach durch Anführungszeichen in der Parser-Sprache abgeglichen. Anführungszeichen werden normalerweise nicht beibehalten. Modifikatoren können verwendet werden. A + hält die Zeichenfolge übereinstimmend. (dh + '-' stimmt mit einem Zeichen überein, das das Zeichen bei Erfolg beibehält.) Die Operation, (dh 'E') fügt die Zeichenfolge in das Token ein. Leerzeichen werden durch die Tokenformel behandelt, die führende SKIP_CLASS-Zeichen überspringt, bis eine erste Übereinstimmung hergestellt wird. Beachten Sie, dass eine explizite Übereinstimmung mit den Zeichen "skip_class" das Überspringen stoppt, sodass ein Token mit einem Zeichen "skip_class" beginnen kann. Die Zeichenfolgen-Token-Formel überspringt führende skip_class-Zeichen, die mit einem einfachen Anführungszeichen oder einer doppelten Zeichenfolge übereinstimmen. Von Interesse ist die Übereinstimmung eines "Zeichens innerhalb einer" Zeichenfolge in Anführungszeichen:
Die erste Alternative entspricht einem einfachen Anführungszeichen. Die richtige Alternative entspricht einer Zeichenfolge in doppelten Anführungszeichen, die doppelte Anführungszeichen enthalten kann, wobei zwei "Zeichen zusammen verwendet werden, um ein einzelnes" Zeichen darzustellen. Diese Formel definiert die in ihrer eigenen Definition verwendeten Zeichenfolgen. Die innere rechte Alternative '"' $ (-" "" ".ANY |" "" "", "" "") '"' entspricht einer doppelten Anführungszeichenfolge. Wir können ein einfaches Anführungszeichen verwenden, um mit einem doppelten Anführungszeichen übereinzustimmen. Wenn wir jedoch innerhalb der doppelten Anführungszeichenfolge ein Zeichen verwenden möchten, müssen wir zwei Zeichen verwenden, um eines zu erhalten. Zum Beispiel in der inneren linken Alternative, die mit einem beliebigen Zeichen außer einem Zitat übereinstimmt:
Ein negativer Blick voraus - "" "wird verwendet, der bei Erfolg (ohne Übereinstimmung mit einem" Zeichen) mit jedem Zeichen übereinstimmt (das kein "Zeichen" sein kann, weil - "" "diese Möglichkeit beseitigt hat). Die richtige Alternative ist es, "" "" einen Charakter zu finden und zu scheitern, waren die richtige Alternative:
versucht, zwei "Zeichen, die sie durch ein einzelnes Doppel ersetzen, mit", "" zu vergleichen, um das einzelne "Zeichen einzufügen. Beide inneren Alternativen, bei denen das schließende Zeichen in Anführungszeichen nicht erfüllt ist, werden abgeglichen und MAKSTR [] wird aufgerufen, um ein Zeichenfolgenobjekt zu erstellen Sequenz, Schleife, während erfolgreich, Operator wird zum Abgleichen einer Sequenz verwendet. Token-Formel überspringen führende Zeichen der Sprungklasse (mit Leerzeichen). Sobald eine erste Übereinstimmung hergestellt wurde, ist das Überspringen von skip_class deaktiviert. Mit []. MAKSTR können in anderen Sprachen programmierte Funktionen aufgerufen werden [], MAKBIN [], MAKOCT [], MAKHEX [], MAKFLOAT [] und MAKINT [] werden mit Bibliotheksfunktionen geliefert, die eine übereinstimmende Tokenzeichenfolge in ein typisiertes Objekt konvertieren. Die folgende Zahlenformel veranschaulicht eine recht komplexe Tokenerkennung:
Die obige Zahlentokenformel erkennt Ganzzahl- und Gleitkommazahlen. Die - Alternativen sind immer erfolgreich. Bei Berechnungen können numerische Objekte verwendet werden. Die Token-Objekte werden nach Erfolg der Formel auf den Analysestapel verschoben. Interessant ist die Exponentenführung in (+ 'E' | 'e', 'E'). Wir möchten immer ein Großbuchstabe E für MAKEFLOAT [] haben. Wir erlauben jedoch, dass ein 'e' in Kleinbuchstaben durch 'E' ersetzt wird.
Möglicherweise haben Sie Konsistenzen zwischen Zeichenklasse und Tokenformel festgestellt. Die Parsing-Formel setzt das Hinzufügen von Backtracking-Alternativen und Baumkonstruktionsoperatoren fort. Alternative Operatoren für das Zurückverfolgen und Nicht-Zurückverfolgen dürfen nicht innerhalb einer Ausdrucksebene gemischt werden. Möglicherweise müssen Sie nicht (a | b \ c) Non-Backtracking | mischen mit \ backtracking Alternative. (a \ b \ c), (a | b | c) und ((a | b) \ c) sind gültig. Eine \ backtracking-Alternative speichert den Analysezustand, bevor die linke Alternative versucht wird, und stellt bei einem Fehler den Analysezustand wieder her, bevor die rechte Alternative versucht wird. In einer Folge von Alternativen erfüllt die erste erfolgreiche Alternative die Gruppe. Weitere Alternativen werden nicht versucht. Factoring und Gruppierung sorgen für eine kontinuierliche Analyse. Die Backtrack-Alternative erstellt einen gespeicherten Status der Analyse, bevor sie die linke Alternative versucht. Backtracking ist erforderlich, wenn die Analyse möglicherweise teilweise übereinstimmt und dann fehlschlägt:
Im obigen Fall wird bei einem Rückgabefehler die alternative CD versucht. Wenn dann c einen Fehler zurückgibt, wird die Backtrack-Alternative versucht. Wenn a erfolgreich ist und b fehlschlägt, wird die Analyse zurückverfolgt und versucht. Ebenso ist ein fehlgeschlagenes c erfolgreich und b fehlgeschlagen. Die Analyse wird zurückverfolgt und die Alternative e genommen. Das Backtracking ist nicht auf eine Formel beschränkt. Wenn eine Analyseformel zu irgendeinem Zeitpunkt teilweise übereinstimmt und dann fehlschlägt, wird die Analyse auf den oberen Backtrack zurückgesetzt und ihre Alternative gewählt. Ein Kompilierungsfehler kann auftreten, wenn Code ausgegeben wurde und der Backtrack erstellt wurde. Vor dem Start der Kompilierung wird ein Backtrack gesetzt. Das Zurückgeben eines Fehlers oder das Zurückverfolgen ist ein Compilerfehler. Backtracks werden gestapelt. Wir können negativ verwenden - und positiv? Peek / Look-Ahead-Operatoren zum Testen, ohne die Analyse voranzutreiben. Ein String-Test ist ein Blick voraus, bei dem nur der Eingabestatus gespeichert und zurückgesetzt werden muss. Ein Blick nach vorne wäre ein Parsing-Ausdruck, der eine teilweise Übereinstimmung ergibt, bevor er fehlschlägt. Ein Blick nach vorne wird durch Backtracking implementiert.
Die Parser-Sprache ist weder ein LL- noch ein LR-Parser. Aber eine Programmiersprache zum Schreiben eines rekursiven anständigen Parsers, in dem Sie die Baumkonstruktion programmieren:
Ein häufig verwendetes Parsing-Beispiel ist ein arithmetischer Ausdruck:
Exp und Term mit einer Schleife erstellen einen Baum für Linkshänder. Der Faktor, der die rechte Rekursion verwendet, erzeugt einen rechtshändigen Baum:
Hier ist ein Teil des cc-Compilers, einer aktualisierten Version von SLIC mit Kommentaren im c-Stil. Funktionstypen (Grammatik, Token, Zeichenklasse, Generator, PSEUDO oder MACHOP) werden durch ihre anfängliche Syntax nach ihrer ID bestimmt. Mit diesen Top-Down-Parsern beginnen Sie mit einer programmdefinierenden Formel:
// Beachten Sie, wie die ID beim Erstellen des Baums berücksichtigt und später kombiniert wird.
Bemerkenswert ist, wie die Parser-Sprache mit Kommentaren und der Fehlerbehebung umgeht.
Ich glaube, ich habe die Frage beantwortet. Nachdem er einen großen Teil des SLIC-Nachfolgers geschrieben hat, ist die CC-Sprache an sich hier. Es gibt noch keinen Compiler dafür. Aber ich kann es von Hand in Assembler-Code kompilieren, nackte asm c- oder c ++ - Funktionen.
quelle
Ja, Sie können einen Compiler für eine Sprache in dieser Sprache schreiben. Nein, Sie benötigen keinen ersten Compiler, damit diese Sprache bootet.
Was Sie zum Bootstrap benötigen, ist eine Implementierung der Sprache. Das kann entweder ein Compiler oder ein Interpreter sein.
Historisch gesehen wurden Sprachen normalerweise entweder als interpretierte oder als kompilierte Sprachen angesehen. Dolmetscher wurden nur für die ersteren und Compiler nur für die letzteren geschrieben. Wenn also ein Compiler für eine Sprache geschrieben werden soll, wird der erste Compiler normalerweise in einer anderen Sprache geschrieben, um ihn zu booten, und optional wird der Compiler für die betreffende Sprache neu geschrieben. Es ist jedoch eine Option, stattdessen einen Dolmetscher in einer anderen Sprache zu schreiben.
Das ist nicht nur theoretisch. Ich mache das gerade selbst. Ich arbeite an einem Compiler für eine Sprache, Salmon, die ich selbst entwickelt habe. Ich habe zuerst einen Salmon-Compiler in C erstellt und jetzt schreibe ich den Compiler in Salmon, damit ich den Salmon-Compiler zum Laufen bringen kann, ohne jemals einen Compiler für Salmon in einer anderen Sprache geschrieben zu haben.
quelle
Vielleicht können Sie eine BNF schreiben, die BNF beschreibt.
quelle