Es ist ein großes Thema, aber anstatt dich mit einem pompösen "Geh, lies ein Buch, Junge" abzuwischen, gebe ich dir gerne Hinweise, die dir helfen, deinen Kopf darum zu wickeln.
Die meisten Compiler und / oder Interpreten arbeiten folgendermaßen:
Tokenize : Scannen Sie den Codetext und teilen Sie ihn in eine Liste von Token auf.
Dieser Schritt kann schwierig sein, da Sie die Zeichenfolge nicht einfach auf Leerzeichen aufteilen können. Sie müssen erkennen, dass if (bar) foo += "a string";
es sich um eine Liste von 8 Token handelt: WORD, OPEN_PAREN, WORD, CLOSE_PAREN, WORD, ASIGNMENT_ADD, STRING_LITERAL, TERMINATOR. Wie Sie sehen, funktioniert es nicht, den Quellcode einfach in Leerzeichen aufzuteilen. Sie müssen jedes Zeichen als Sequenz lesen. Wenn Sie also auf ein alphanumerisches Zeichen stoßen, lesen Sie solange Zeichen, bis Sie ein nicht-alphanumerisches Zeichen und diese Zeichenfolge treffen Gerade gelesen ist ein Wort, das später weiter klassifiziert werden soll. Sie können selbst entscheiden, wie detailliert Ihr Tokenizer ist: ob er "a string"
als ein Token namens STRING_LITERAL verschluckt wird, das später weiter analysiert wird, oder ob er dies sieht"a string"
OPEN_QUOTE, UNPARSED_TEXT, CLOSE_QUOTE oder was auch immer, dies ist nur eine von vielen Möglichkeiten, die Sie beim Codieren für sich entscheiden müssen.
Lex : Nun haben Sie eine Liste von Token. Sie haben wahrscheinlich einige Token mit einer mehrdeutigen Klassifizierung wie WORD versehen, weil Sie beim ersten Durchgang nicht zu viel Mühe darauf verwenden, den Kontext der einzelnen Zeichenfolgen zu ermitteln. Lesen Sie nun Ihre Liste der Quell-Token erneut und klassifizieren Sie jeden der mehrdeutigen Token mit einem spezifischeren Token-Typ, basierend auf den Schlüsselwörtern in Ihrer Sprache. Sie haben also ein WORT wie "if" und "if" in Ihrer Liste der speziellen Schlüsselwörter, die als "Symbol IF" bezeichnet werden, sodass Sie den Symboltyp dieses Tokens von "WORD" in "IF" ändern und jedes WORT, das sich nicht in Ihrer Liste der speziellen Schlüsselwörter befindet , wie WORD foo, ist ein IDENTIFIER.
Parse : Sie haben jetzt if (bar) foo += "a string";
eine Liste mit lexierten Token erstellt, die folgendermaßen aussieht: IF OPEN_PAREN IDENTIFER CLOSE_PAREN IDENTIFIER ASIGN_ADD STRING_LITERAL TERMINATOR. Der Schritt besteht darin, Folgen von Tokens als Anweisungen zu erkennen. Das ist Parsen. Dazu verwenden Sie eine Grammatik wie:
STATEMENT: = ASIGN_EXPRESSION | IF_STATEMENT
IF_STATEMENT: = IF, PAREN_EXPRESSION, STATEMENT
ASIGN_EXPRESSION: = IDENTIFIER, ASIGN_OP, VALUE
PAREN_EXPRESSSION: = OPEN_PAREN, VALUE, CLOSE_PAREN
VALUE: = IDENTIFIER | STRING_LITERAL | PAREN_EXPRESSION
ASIGN_OP: = EQUAL | ASIGN_ADD | ASIGN_SUBTRACT | ASIGN_MULT
Die Produktionen, die "|" zwischen Begriffen bedeutet "Übereinstimmung mit diesen", wenn es Kommas zwischen Begriffen gibt, bedeutet dies "Übereinstimmung mit dieser Abfolge von Begriffen"
Wie benutzt du das? Versuchen Sie, beginnend mit dem ersten Token, Ihre Token-Sequenz mit diesen Produktionen abzugleichen. Also versuchen Sie zuerst, Ihre Token-Liste mit STATEMENT abzugleichen, also lesen Sie die Regel für STATEMENT und es heißt "ein STATEMENT ist entweder ein ASIGN_EXPRESSION oder ein IF_STATEMENT", also versuchen Sie zuerst, ASIGN_EXPRESSION abzugleichen, also schlagen Sie die Grammatikregel für ASIGN_EXPRESSION nach und es heißt "ASIGN_EXPRESSION ist ein IDENTIFIER gefolgt von einem ASIGN_OP gefolgt von einem VALUE, so dass Sie die Grammatikregel für IDENTIFIER nachschlagen und sehen, dass es für IDENTIFIER keine Grammatiklücke gibt, was bedeutet, dass IDENTIFIER ein" Terminal "ist, was bedeutet, dass es nicht weiter benötigt Analysieren, um es abzugleichen, damit Sie versuchen können, es direkt mit Ihrem Token abzugleichen. Ihr erstes Quelltoken ist jedoch eine IF, und IF ist nicht dasselbe wie ein IDENTIFIER. Was jetzt? Sie kehren zur Regel STATEMENT zurück und versuchen, den nächsten Ausdruck zu finden: IF_STATEMENT. Sie suchen nach IF_STATEMENT, es beginnt mit IF, suchen nach IF, IF ist ein Terminal, vergleichen Sie das Terminal mit Ihrem ersten Token, IF-Token-Übereinstimmungen, sehen Sie nach PAREN_EXPRESSION, suchen Sie nach PAREN_EXPRESSION, es ist kein Terminal, was ist es, PAREN_EXPRESSION beginnt mit OPEN_PAREN, sucht nach OPEN_PAREN, ist ein Terminal, ordnet OPEN_PAREN Ihrem nächsten Token zu, stimmt überein, ... und so weiter.
Der einfachste Weg, sich diesem Schritt zu nähern, besteht darin, eine Funktion namens parse () zu verwenden, mit der Sie das Quelltext-Token, mit dem Sie übereinstimmen möchten, und den Grammatikbegriff übergeben, mit dem Sie übereinstimmen möchten. Wenn der Grammatikbegriff kein Terminal ist, verwenden Sie erneut: Sie rufen parse () auf und übergeben ihm erneut das gleiche Quell-Token und den ersten Begriff dieser Grammatikregel. Aus diesem Grund wird es als "rekursiver Abstiegsparser" bezeichnet. Die Funktion parse () gibt Ihre aktuelle Position beim Lesen der Quelltoken zurück (oder ändert sie). Sie gibt im Wesentlichen das letzte Token in der übereinstimmenden Sequenz zurück und Sie fahren mit dem nächsten Aufruf von fort parse () von dort.
Jedes Mal, wenn parse () mit einer Produktion wie ASIGN_EXPRESSION übereinstimmt, erstellen Sie eine Struktur, die diesen Code darstellt. Diese Struktur enthält Verweise auf die ursprünglichen Quelltoken. Sie beginnen mit der Erstellung einer Liste dieser Strukturen. Wir nennen diese gesamte Struktur den Abstract Syntax Tree (AST).
Kompilieren und / oder Ausführen : Für bestimmte Produktionen in Ihrer Grammatik haben Sie Handlerfunktionen erstellt, die bei einer AST-Struktur diesen AST-Block kompilieren oder ausführen.
Schauen wir uns also das Teil Ihres AST an, das den Typ ASIGN_ADD hat. Als Interpreter haben Sie also eine ASIGN_ADD_execute () -Funktion. Diese Funktion wird als Teil des AST übergeben, der dem Analysebaum für entspricht. Daher betrachtet foo += "a string"
diese Funktion diese Struktur und weiß, dass der erste Term in der Struktur ein IDENTIFIER sein muss und der zweite Term der VALUE ist. ASIGN_ADD_execute () Übergibt den VALUE-Term an eine VALUE_eval () -Funktion, die ein Objekt zurückgibt, das den ausgewerteten Wert im Speicher darstellt. Dann sucht ASIGN_ADD_execute () in Ihrer Variablentabelle nach "foo" und speichert einen Verweis auf alles, was von eval_value () zurückgegeben wurde. Funktion.
Das ist ein Dolmetscher. Ein Compiler hätte stattdessen Handlerfunktionen, die den AST in Bytecode oder Maschinencode übersetzen, anstatt ihn auszuführen.
Die Schritte 1 bis 3 und einige 4 können mithilfe von Tools wie Flex und Bison vereinfacht werden. (aka. Lex und Yacc), aber selbst einen Dolmetscher zu schreiben, ist wahrscheinlich die stärkste Übung, die ein Programmierer erreichen kann. Alle anderen Programmierherausforderungen scheinen nach diesem Gipfel trivial zu sein.
Mein Rat ist, klein anzufangen: eine winzige Sprache mit einer winzigen Grammatik, und zu versuchen, ein paar einfache Aussagen zu analysieren und auszuführen, und von dort aus zu wachsen.
Lesen Sie diese und viel Glück!
http://www.iro.umontreal.ca/~felipe/IFT2030-Automne2002/Complements/tinyc.c
http://en.wikipedia.org/wiki/Recursive_descent_parser
lex
,yacc
undbison
.