Die meisten Compiler haben die folgende Architektur:

In diesem Artikel werde ich diese Architektur Element für Element im Detail analysieren.
Wir können sagen, dass dieser Artikel eine Ergänzung zu der großen Menge vorhandener Ressourcen zum Thema Compiler ist. Es ist eine autonome Quelle, die es Ihnen ermöglicht, die Grundlagen des Entwurfs und der Implementierung von Programmiersprachen zu verstehen.
Die Zielgruppe des Artikels sind Personen, deren Vorstellung von der Arbeit der Compiler äußerst begrenzt ist (das Maximum ist, dass sie am Kompilieren beteiligt sind). Ich erwarte jedoch, dass der Leser Datenstrukturen und Algorithmen versteht.
Der Artikel ist keineswegs modernen Produktions-Compilern mit Millionen von Codezeilen gewidmet - nein, dies ist ein kurzer Kurs „Compiler für Dummies“, der Ihnen hilft, zu verstehen, was ein Compiler ist.
Einführung
Ich arbeite derzeit an der von Rust and Go inspirierten
Krug- Systemsprache. In dem Artikel werde ich Krug als Beispiel nennen, um meine Gedanken zu veranschaulichen. Krug befindet sich in der Entwicklung, ist jedoch bereits unter
https://github.com/krug-lang in den
Caasper- und
Krug- Repositories verfügbar. Die Sprache ist im Vergleich zur üblichen Compiler-Architektur nicht ganz typisch, was mich teilweise dazu inspiriert hat, einen Artikel zu schreiben - aber dazu später mehr.
Ich beeile mich, Ihnen mitzuteilen, dass ich in keiner Weise ein Spezialist für Compiler bin! Ich habe keinen Doktortitel und keine formelle Ausbildung absolviert. In meiner Freizeit habe ich alles, was im Artikel beschrieben ist, selbst studiert. Ich muss auch sagen, dass ich nicht den tatsächlichen, nur richtigen Ansatz zum Erstellen eines Compilers beschreibe, sondern die grundlegenden Methoden vorstelle, die zum Erstellen eines kleinen "Spielzeug" -Compilers geeignet sind.
Frontend
Kehren wir zum obigen Diagramm zurück: Die Pfeile links, die auf das Frontendfeld gerichtet sind, sind bekannte und beliebte Sprachen wie C. Das Frontend sieht ungefähr so aus: lexikalische Analyse -> Parser.
Lexikalische Analyse
Als ich anfing, Compiler und Sprachdesign zu studieren, wurde mir gesagt, dass lexikalische Analyse dasselbe ist wie Tokenisierung. Wir werden diese Beschreibung verwenden. Der Analysator nimmt Eingabedaten in Form von Zeichenfolgen oder einem Strom von Zeichen auf und erkennt darin Muster, die er in Token schneidet.
Im Falle eines Compilers erhält er am Eingang ein geschriebenes Programm. Es wird aus einer Datei in eine Zeichenfolge eingelesen, und der Analysator markiert seinen Quellcode.
enum TokenType { Identifier, Number, }; struct Token { std::string Lexeme; TokenType type; // ... // It's also handy to store things in here // like the position of the token (start to end row:col) };
In diesem Fragment, das in einer C-förmigen Sprache geschrieben ist, sehen Sie die Struktur, die das oben genannte Lexem enthält, sowie TokenType, das zur Erkennung dieses Tokens dient.
Hinweis: Der Artikel ist keine Anleitung zum Erstellen einer Sprache mit Beispielen. Zum besseren Verständnis werde ich jedoch von Zeit zu Zeit Codefragmente einfügen.
Analysatoren sind normalerweise die einfachsten Compilerkomponenten. Das gesamte Frontend ist im Vergleich zu den anderen Puzzleteilen recht einfach. Obwohl es sehr von Ihrer Arbeit abhängt.
Nehmen Sie den folgenden C-Code:
int main() { printf("Hello world!\n"); return 0; }
Nachdem Sie es aus einer Datei in eine Zeile gelesen und einen linearen Scan durchgeführt haben, können Sie möglicherweise Token in Scheiben schneiden. Wir identifizieren Token auf natürliche Weise - da int ein "Wort" und 0 in der return-Anweisung eine "Zahl" ist. Der lexikalische Analysator führt das gleiche Verfahren wie wir aus - später werden wir diesen Prozess genauer untersuchen. Analysieren Sie beispielsweise die Zahlen:
0xdeadbeef — HexNumber ( ) 1231234234 — WholeNumber ( ) 3.1412 — FloatingNumber ( ) 55.5555 — FloatingNumber ( ) 0b0001 — BinaryNumber ( )
Das Definieren von Wörtern kann schwierig sein. Die meisten Sprachen definieren ein Wort als eine Folge von Buchstaben und Zahlen, und ein Bezeichner sollte normalerweise mit einem Buchstaben oder einem Unterstrich beginnen. Zum Beispiel:
123foobar := 3 person-age := 5 fmt.Println(123foobar)
In Go wird dieser Code nicht als korrekt angesehen und in die folgenden Token analysiert:
Number(123), Identifier(foobar), Symbol(:=), Number(3) ...
Die meisten der gefundenen Bezeichner sehen folgendermaßen aus:
foo_bar __uint8_t fooBar123
Analysatoren müssen andere Probleme lösen, z. B. Leerzeichen, mehrzeilige und einzeilige Kommentare, Bezeichner, Zahlen, Zahlensysteme und Zahlenformatierung (z. B. 1_000_000) und Codierungen (z. B. Unterstützung für UTF8 anstelle von ASCII).
Und wenn Sie denken, Sie können auf reguläre Ausdrücke zurückgreifen - besser nicht wert. Es ist viel einfacher, einen Analysator von Grund auf neu zu schreiben, aber ich empfehle dringend,
diesen Artikel von unserem König und Gott Rob Pike zu lesen. Die Gründe, warum Regex für uns nicht geeignet ist, sind in vielen anderen Artikeln beschrieben, daher werde ich diesen Punkt weglassen. Darüber hinaus ist das Schreiben eines Analysators viel interessanter, als sich mit langen, ausführlichen Ausdrücken zu quälen, die um 5:24 Uhr auf regex101.com hochgeladen wurden. In meiner ersten Sprache habe ich die Funktion
split(str)
für die Tokenisierung verwendet - und bin nicht weit gegangen.
Parsen
Das Parsen ist etwas komplizierter als die lexikalische Analyse. Es gibt viele Parser und Parser-Generatoren - hier beginnt das Spiel in großem Stil.
Parser in Compilern nehmen normalerweise Eingaben in Form von Token entgegen und erstellen einen bestimmten Baum - einen abstrakten Syntaxbaum oder einen Parsing-Baum. Sie sind von Natur aus ähnlich, weisen jedoch einige Unterschiede auf.
Diese Stufen können als Funktionen dargestellt werden:
fn lex(string input) []Token {...} fn parse(tokens []Token) AST {...} let input = "int main() { return 0; }"; let tokens = lex(input); let parse_tree = parse(tokens); // ....
In der Regel bestehen Compiler aus vielen kleinen Komponenten, die Eingaben übernehmen, ändern oder in andere Ausgaben konvertieren. Dies ist einer der Gründe, warum funktionale Sprachen gut zum Erstellen von Compilern geeignet sind. Andere Gründe sind ausgezeichnetes Benchmarking und ziemlich umfangreiche Standardbibliotheken. Unterhaltsame Tatsache: Die erste Implementierung des
Rust- Compilers war auf Ocaml.
Ich rate Ihnen, diese Komponenten so einfach und autonom wie möglich zu halten - Modularität erleichtert den Prozess erheblich. Meiner Meinung nach gilt das Gleiche für viele andere Aspekte der Softwareentwicklung.
Bäume
Parsing-Baum
Was zum Teufel ist das? Dieser dicke Baum wird auch als Analysebaum bezeichnet und dient zur Visualisierung des Quellprogramms. Sie enthalten alle Informationen (oder die meisten davon) über das Eingabeprogramm, normalerweise die gleichen wie in der Grammatik Ihrer Sprache beschrieben. Jeder Baumknoten ist nachlaufend oder nicht nachlaufend, z. B. NumberConstant oder StringConstant.
Abstrakter Syntaxbaum
Wie der Name schon sagt, ist die ASD ein
abstrakter Syntaxbaum. Der Analysebaum enthält viele (häufig redundante) Informationen zu Ihrem Programm. Bei einer ASD sind diese nicht erforderlich. ASD benötigt keine nutzlosen Informationen über die Struktur und Grammatik, was die Semantik des Programms nicht beeinflusst.
Angenommen, Ihr Baum hat einen Ausdruck wie ((5 + 5) -3) +2. Im Analysebaum würden Sie ihn zusammen mit Klammern, Operatoren und Werten 5, 5, 3 und 2 vollständig speichern. Sie können ihn jedoch einfach mit der ASD verknüpfen - wir müssen nur die Werte, Operatoren und deren Reihenfolge kennen.
Das Bild unten zeigt den Baum für den Ausdruck a + b / c.
ASD kann wie folgt dargestellt werden:
interface Expression { ... }; struct UnaryExpression { Expression value; char op; }; struct BinaryExpression { Expression lhand, rhand; string op; // string because the op could be more than 1 char. }; interface Node { ... }; // or for something like a variable struct Variable : Node { Token identifier; Expression value; };
Diese Ansicht ist ziemlich eingeschränkt, aber ich hoffe, Sie können sehen, wie Ihre Knoten strukturiert werden. Zum Parsen können Sie wie folgt vorgehen:
Node parseNode() { Token current = consume(); switch (current.lexeme) { case "var": return parseVariableNode(); // ... } panic("unrecognized input!"); } Node n = parseNode(); if (n != null) { // append to some list of top level nodes? // or append to a block of nodes! }
Ich hoffe, dass Sie einen Überblick darüber erhalten, wie das schrittweise Parsen der verbleibenden Knoten ablaufen wird, beginnend mit Sprachkonstrukten auf hoher Ebene. Wie genau ein Parser mit rekursiver Abstammung implementiert ist, müssen Sie selbst studieren.
Grammatik
Das Parsen in einem ADS aus einer Reihe von Token kann schwierig sein. Normalerweise sollten Sie mit der Grammatik Ihrer Sprache beginnen. Im Wesentlichen bestimmt die Grammatik die Struktur Ihrer Sprache. Es gibt mehrere Sprachen zum Definieren von Sprachen, die sich selbst beschreiben (oder analysieren) können.
Ein Beispiel für eine Sprache zur Bestimmung von Sprachen ist eine
erweiterte Form von Backus-Naur (RBNF). Es ist eine Variante von
BNF mit weniger spitzen Klammern. Hier ist ein RBNF-Beispiel aus einem Wikipedia-Artikel:
digit excluding zero = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; digit = "0" | digit excluding zero ;
Produktionsregeln sind definiert: Sie geben an, welche Terminalvorlage „nicht terminal“ ist. Terminals sind Teil des Alphabets, z. B. sind das if-Token oder 0 und 1 im obigen Beispiel Terminals. Nicht-Terminals sind das Gegenteil, sie befinden sich auf der linken Seite der Produktionsregeln und können als Variablen oder „benannte Zeiger“ auf Gruppen von Terminals und Nicht-Terminals betrachtet werden.
Viele Sprachen haben Spezifikationen, die Grammatik enthalten. Zum Beispiel für
Go ,
Rust und
D.Rekursive Abstiegsanalysatoren
Rekursiver Abstieg ist der einfachste von vielen Parsing-Ansätzen.
Rekursive Abstiegsanalysatoren - absteigend, basierend auf rekursiven Verfahren. Es ist viel einfacher, einen Parser zu schreiben, da Ihre Grammatik keine
Rekursion hinterlassen hat . In den meisten "Spielzeug" -Sprachen reicht diese Technik zum Parsen aus. GCC verwendet einen handgeschriebenen absteigenden Parser, obwohl YACC zuvor verwendet wurde.
Das Parsen dieser Sprachen kann jedoch zu Problemen führen. Insbesondere C, wo
foo * bar
kann interpretiert werden als
int foo = 3; int bar = 4; foo * bar; // unused expression
oder wie
typedef struct { int b; } foo; foo* bar; bar.b = 3;
Die Clang-Implementierung verwendet auch einen rekursiven Abstiegsanalysator:
Da dies regulärer C ++ - Code ist, erleichtert ein rekursiver Abstieg Anfängern das Verständnis. Es unterstützt benutzerdefinierte Regeln und andere seltsame Dinge, die C / C ++ erfordert, und hilft Ihnen, Fehler einfach zu diagnostizieren und zu beheben.Es lohnt sich auch, auf andere Ansätze zu achten:
- absteigender LL, rekursiver Abstieg
- aufsteigender LR, Verschiebung, aufsteigender Abstieg
Parser-Generatoren
Ein weiterer guter Weg. Natürlich gibt es auch Nachteile - aber dies kann über jede andere Wahl gesagt werden, die Programmierer beim Erstellen von Software treffen.
Parser-Generatoren arbeiten sehr zügig. Ihre Verwendung ist einfacher als das Schreiben eines eigenen Analysators und das Erhalten eines Qualitätsergebnisses - obwohl sie nicht sehr benutzerfreundlich sind und nicht immer Fehlermeldungen anzeigen. Außerdem müssen Sie lernen, wie Sie den Parser-Generator verwenden, und wenn Sie den Compiler hochstufen, müssen Sie wahrscheinlich den Parser-Generator abwickeln.
Ein Beispiel für einen Parser-Generator ist
ANTLR , es gibt viele andere.
Ich denke, dieses Tool ist für diejenigen geeignet, die keine Zeit damit verbringen möchten, ein Frontend zu schreiben, und die es vorziehen, die Mitte und das Backend des Compilers / Interpreters zu schreiben und irgendetwas zu analysieren.
Analyseanwendung
Wenn du dich immer noch nicht verstehst. Sogar das Compiler-Frontend (lex / parse) kann verwendet werden, um andere Probleme zu lösen:
- Syntaxhervorhebung
- HTML / CSS-Analyse für die Rendering-Engine
- Transpiler: TypeScript, CoffeeScript
- Linker
- REGEX
- Schnittstellendatenanalyse
- URL-Analyse
- Formatierungswerkzeuge wie gofmt
- SQL-Analyse und mehr.
Mitte
Semantische Analyse! Die Analyse der Sprachsemantik ist eine der schwierigsten Aufgaben beim Erstellen eines Compilers.
Sie müssen sicherstellen, dass alle Eingabeprogramme ordnungsgemäß funktionieren. In meiner Krug-Sprache wurden Aspekte der semantischen Analyse noch nicht berücksichtigt, und ohne sie muss der Programmierer immer den richtigen Code schreiben. In Wirklichkeit ist dies unmöglich - und wir schreiben, kompilieren, führen manchmal Fehler aus. Diese Spirale ist endlos.
Darüber hinaus ist die Kompilierung von Programmen ohne eine Analyse der Richtigkeit der Semantik in der entsprechenden Phase der Kompilierung nicht möglich.
Ich bin einmal auf ein Diagramm über den Prozentsatz von Frontend, Midland und Backend gestoßen. Dann sah es so aus
F: 20% M: 20%: B: 60%
Heute ist es so etwas wie
F: 5% M: 60% B: 35%
Das Frontend befasst sich hauptsächlich mit dem Generator, und in kontextlosen Sprachen, die nicht die Dualität der Grammatik haben, können sie recht schnell abgeschlossen werden - ein rekursiver Abstieg hilft hier.
Mit der LLVM-Technologie kann der größte Teil der Optimierungsarbeit in das Framework hochgeladen werden, das eine Reihe vorgefertigter Optimierungen enthält.
Der nächste Schritt ist die semantische Analyse, ein wesentlicher Bestandteil der Kompilierungsphase.
In Rust mit seinem Speicherverwaltungsmodell fungiert der Compiler beispielsweise als große, leistungsstarke Maschine, die verschiedene Arten der statischen Analyse von Einführungsformularen durchführt. Ein Teil dieser Aufgabe besteht darin, die Eingabedaten in eine bequemere Form für die Analyse umzuwandeln.
Aus diesem Grund spielt die semantische Analyse eine wichtige Rolle in der Architektur des Compilers, und anstrengende Vorbereitungsarbeiten wie die Optimierung der generierten Assembly oder das Lesen der Eingabedaten in der ASD werden für Sie durchgeführt.
Semantische Passagen
Im Verlauf der semantischen Analyse führen die meisten Compiler eine große Anzahl von "semantischen Durchläufen" auf dem SDA oder einer anderen abstrakten Form des Code-Ausdrucks durch.
Dieser Artikel enthält Details zu den meisten im .NET C # -Compiler durchgeführten Durchläufen.
Ich werde nicht jede Passage betrachten, zumal sie je nach Sprache variieren, aber einige Schritte werden unten in Krug beschrieben.
Top Level Ad
Der Compiler durchläuft alle "Top-Level" -Ankündigungen in den Modulen und ist sich ihrer Existenz bewusst. Er wird nicht tiefer in Blöcke gehen - er wird einfach erklären, welche Strukturen, Funktionen usw. sind in dem einen oder anderen Modul verfügbar.
Namens- / Symbolauflösung
Der Compiler durchläuft alle Codeblöcke in Funktionen usw. und löst sie auf - das heißt, es werden Zeichen gefunden, für die eine Berechtigung erforderlich ist. Dies ist ein häufiger Durchgang, und von hier aus tritt beim Kompilieren von Go-Code normalerweise der Fehler "
Kein solches Symbol XYZ" auf .
Das Durchführen dieses Durchlaufs kann sehr schwierig sein, insbesondere wenn Ihr Abhängigkeitsdiagramm zirkuläre Abhängigkeiten enthält. Einige Sprachen erlauben sie nicht, zum Beispiel, Go gibt einen Fehler aus, wenn eines Ihrer Pakete eine Schleife bildet, wie meine Krug-Sprache. Zyklische Abhängigkeiten können als Nebeneffekt einer schlechten Architektur angesehen werden.
Schleifen können durch Ändern der DFS im Abhängigkeitsdiagramm oder durch Verwenden
des Tarjan-Algorithmus (wie von Krug ausgeführt) zum Definieren (mehrerer) Schleifen bestimmt werden.
Typ Inferenz
Der Compiler durchläuft alle Variablen und zeigt deren Typen an. Die Typinferenz in Krug ist sehr schwach und gibt einfach Variablen basierend auf ihren Werten aus. Es ist keineswegs ein bizarres System, wie man es in funktionalen Sprachen wie Haskell findet.
Typen können mithilfe des Prozesses "Vereinheitlichung" oder "Vereinheitlichung von Typen" abgeleitet werden. Für einfachere Typsysteme kann eine sehr einfache Implementierung verwendet werden.
Typen werden in Krug folgendermaßen implementiert:
interface Type {}; struct IntegerType : Type { int width; bool signed; }; struct FloatingType : Type { int width; }; struct ArrayType : Type { Type base_type; uint64 length; };
Sie können auch eine einfache Typinferenz verwenden, bei der Sie Ausdrucksknoten einen Typ zuweisen. Beispielsweise kann
IntegerConstantNode
vom Typ IntegerType (64) sein. Und dann erhalten Sie möglicherweise die Funktion
unify(t1, t2)
, mit der der breiteste Typ ausgewählt wird, mit dem der Typ komplexerer Ausdrücke abgeleitet werden kann, z. B. binäre. Es geht also darum, den Werten der angegebenen Typen rechts eine Variable auf der linken Seite zuzuweisen.
Ich habe einmal eine einfache
Besetzung von Go geschrieben, die zu einem Prototyp für Krug wurde.
Mutability Pass
Krug (wie Rust) ist standardmäßig eine unveränderliche Sprache, dh Variablen bleiben unverändert, sofern nicht anders angegeben:
let x = 3; x = 4; // BAD! mut y = 5; y = 6; // OK!
Der Compiler durchläuft alle Blöcke und Funktionen und prüft, ob ihre „Variablen korrekt“ sind, dh wir ändern nicht, was nicht folgt, und dass alle an bestimmte Funktionen übergebenen Variablen bei Bedarf konstant oder änderbar sind.
Dies geschieht mit Hilfe symbolischer Informationen, die in früheren Durchgängen gesammelt wurden. Eine Symboltabelle, die auf den Ergebnissen des semantischen Durchlaufs basiert, enthält Token-Namen und Zeichen variabler Variabilität. Es kann andere Daten enthalten, z. B. in C ++ kann eine Tabelle Informationen darüber speichern, ob ein Symbol extern oder statisch ist.
Zeichentabellen
Eine Zeichentabelle oder „Stich“ ist eine Tabelle zum Auffinden der Zeichen, die in Ihrem Programm verwendet werden. Für jeden Bereich wird eine Tabelle erstellt, die alle Informationen zu den in einem bestimmten Bereich vorhandenen Zeichen enthält.
Diese Informationen umfassen Eigenschaften wie den Symbolnamen, den Typ, das Zeichen der Veränderlichkeit, das Vorhandensein externer Kommunikation, den Speicherort im statischen Speicher usw.
Geltungsbereich
Dies ist ein wichtiges Konzept in Programmiersprachen. Natürlich muss Ihre Sprache es nicht ermöglichen, verschachtelte Bereiche zu erstellen, alles kann in einem gemeinsamen Namespace platziert werden!
Obwohl die Darstellung des Bereichs eine interessante Aufgabe für die Compilerarchitektur ist, verhält sich der Bereich in den meisten C-ähnlichen Sprachen wie eine Stapeldatenstruktur (oder ist dies auch).
Normalerweise erstellen und zerstören wir Bereiche, und sie werden normalerweise zum Verwalten von Namen verwendet, dh sie ermöglichen es uns, Variablen zu verbergen (zu beschatten):
{ // push scope let x = 3; { // push scope let x = 4; // OK! } // pop scope } // pop scope
Es kann anders dargestellt werden:
struct Scope { Scope* outer; SymbolTable symbols; }
Ein kleines offtopic, aber ich empfehle, über den
Spaghetti-Stapel zu lesen. Dies ist eine Datenstruktur, die zum Speichern von Sichtbarkeitsbereichen in den ASD-Knoten gegenüberliegender Blöcke verwendet wird.
Typ Systeme
Viele der folgenden Abschnitte können zu separaten Artikeln entwickelt werden, aber es scheint mir, dass dieser Titel dies am meisten verdient. Heutzutage sind viele Informationen über Typsysteme sowie Sorten der Systeme selbst verfügbar, um die herum viele Kopien brechen. Ich werde mich nicht
eingehend mit diesem Thema
befassen, sondern nur einen Link zu dem
ausgezeichneten Artikel von Steve Klabnik hinterlassen .
Ein Typsystem wird im Compiler mithilfe von Compiler-Darstellungen und der Analyse dieser Darstellungen bereitgestellt und semantisch definiert.
Besitz
Dieses Konzept wird in der Programmierung immer häufiger verwendet. Die Prinzipien der Semantik von Eigentum und Bewegung sind in die
Rust- Sprache eingebettet, und ich hoffe, dass sie in anderen Sprachen erscheinen. Rust führt viele verschiedene Arten der statischen Analyse durch, um zu überprüfen, ob die Eingabe eine Reihe von Regeln bezüglich des Speichers erfüllt: Wem gehört welcher Speicher, wann wird der Speicher zerstört und wie viele Verweise (oder Ausleihen) auf diese Werte oder Speicher vorhanden sind.
Das Schöne an Rust liegt in der Tatsache, dass dies alles während der Kompilierung im Compiler erfolgt, sodass sich der Programmierer nicht mit der Speicherbereinigung oder der Linkzählung befassen muss. Alle diese Semantiken sind dem Typsystem zugeordnet und können bereitgestellt werden, noch bevor das Programm in Form einer vollständigen Binärdatei präsentiert wird.
Ich kann nicht sagen, wie das alles unter der Haube funktioniert, aber all dies ist das Ergebnis statischer Analysen und wunderbarer Forschungen des Mozilla-Teams und der
Cyclone- Projektteilnehmer.
Kontrollflussdiagramme
Zur Darstellung von Programmabläufen verwenden wir Kontrollflussdiagramme (CFGs), die alle Pfade enthalten, denen die Programmausführung folgen kann. Dies wird in der semantischen Analyse verwendet, um inaktive Codeabschnitte auszuschließen, dh Blöcke, Funktionen und sogar Module, die während der Programmausführung niemals erreicht werden. Diagramme können auch verwendet werden, um Zyklen zu identifizieren, die nicht unterbrochen werden können. Oder um nach unzugänglichem Code zu suchen, wenn Sie beispielsweise eine „Panik“ (Panik) aufrufen oder in einer Schleife zurückkehren und der Code außerhalb der Schleife nicht ausgeführt wird.
Die Datenflussanalyse spielt in der semantischen Phase des Compilers eine wichtige Rolle. Ich empfehle daher, zu lesen, welche Analysetypen Sie ausführen können, wie sie funktionieren und welche Optimierungen möglich sind.
Backend
Der letzte Teil unseres Architekturschemas.Wir haben den größten Teil der Arbeit zum Generieren ausführbarer Binärdateien geleistet. Dies kann auf verschiedene Arten geschehen, die wir unten diskutieren werden.
- , . , , «».
, . , , . , , , . , .
, . , ++ — Cfront — C.
JavaScript. TypeScript , , , , .
«» , , , , « » . — , , .
LLVM
LLVM: Rust, Swift, C/C++ (clang), D, Haskell.
« », , . , LLVM . , . , , , , 1, 4, 8 16-. , , - .
-
— , — , .
Go — , LLVM ( ). Go , Windows, Linux MacOS. , Krug -.
. , LLVM, , , LLVM , .
, , , LLVM, IR, , , , ( ).
. , , , . IR ( ) «» fprintf .
8cc .
. — Java: ,
JVM , , Kotlin.
, Java . , . , .
, JVM JIT , JIT-, .
, ! , , . - , , .
Godbolt — , , . , , .
, , (strip the debug symbols), , GCC. , - .
. . , . production-.
rwmj , 8 , 80% . 1971-!
, Rust.
IR
(intermediate representation, IR) , . , , .
IR . , , , .
IR, «», IR . , SSA — Static Single Assignment, , .
Go IR SSA. IR LLVM SSA, .
SSA , , (constant propagation), ( ) .
, . , , , , . ( 16 32), , (spill to the stack).
— ( ). , , .
:
- (graph colouring) — (NP- ). , (liveness ranges) .
- — .
. , . , , .
-, , . , .
fn main() int { let x = 0; { let x = 0; { let x = 0; } } return 0; }
, ( - :) ) , . , .
LLDB
DWARF . LLVM , DWARF GNU-. , , , .
(Foreign Function Interface, FFI )
libc , , . , ?
— . , ( .s/.asm)? ? ,
Jai . , .
(CaaS)
API-. , Krug-, . , , .
, , , . , API-.
production- CaaS. Microsofts Roslyn, , . , , , , API-, , , Rust
RLS .
Krug — — Caasper CaaS-.
Caasper (, , ), , krug, . , , (bootstrap) , .
Krug JavaScript, Go*, , , Krug. JavaScript , yarn/npm.
* Go () , JS.Caasper
.
Github Krug, D LLVM. YouTube-
.
Krug ()
.
Nützliche Links