Compiler und virtuelle Maschine
Dieser Abschnitt umfasst Programmkompilierung und Needle-Language-Operationen in der virtuellen Maschine (VM).
Speicherung und Kompilierung des Quellcodes
Verträge und Funktionen werden mit Golang geschrieben und in den Vertragstabellen von Ökosystemen gespeichert.
Wenn ein Vertrag ausgeführt wird, wird sein Quellcode aus der Datenbank gelesen und in Bytecode kompiliert.
Wenn ein Vertrag geändert wird, wird sein Quellcode aktualisiert und in der Datenbank gespeichert. Dann wird der Quellcode kompiliert, wodurch der Bytecode in der entsprechenden virtuellen Maschine aktualisiert wird.
Da Bytecodes nicht physikalisch gespeichert werden, werden sie bei einer erneuten Programmausführung neu kompiliert.
Der gesamte in der Vertragstabelle jedes Ökosystems beschriebene Quellcode wird in einer strengen Reihenfolge in eine virtuelle Maschine kompiliert, und der Status der virtuellen Maschine ist auf allen Knoten gleich.
Beim Vertragsaufruf ändert die virtuelle Maschine ihren Status in keiner Weise. Die Ausführung eines Vertrages oder das Aufrufen einer Funktion erfolgt auf einem separaten laufenden Stack, der während jedes externen Aufrufs erstellt wird.
Jedes Ökosystem kann ein sogenanntes virtuelles Ökosystem haben, das innerhalb eines Knotens in Verbindung mit Tabellen außerhalb der Blockchain verwendet werden kann, ohne direkten Einfluss auf die Blockchain oder andere virtuelle Ökosysteme. In diesem Fall erstellt der Knoten, der ein solches virtuelles Ökosystem hostet, seinen Vertrag und erstellt seine eigene virtuelle Maschine.
Strukturen virtueller Maschinen
VM-Struktur
Eine virtuelle Maschine ist im Arbeitsspeicher als Struktur wie unten organisiert.
Eine VM-Struktur hat die folgenden Elemente:
- Block - enthält eine [Blockstruktur] (#block-structure);
- ExtCost - eine Funktion gibt die Kosten für die Ausführung einer externen Golang-Funktion zurück;
- FuncCallsDB - eine Sammlung von Golang-Funktionsnamen. Diese Funktion gibt die Ausführungskosten als ersten Parameter zurück. Diese Funktionen verwenden EXPLAIN, um die Kosten der Datenbankverarbeitung zu berechnen;
- Extern – ein Boolesches Flag, das angibt, ob ein Vertrag ein externer Vertrag ist. Es wird auf „true“ gesetzt, wenn eine VM erstellt wird. Aufgerufene Verträge werden beim Kompilieren des Codes nicht angezeigt. Mit anderen Worten, es ermöglicht, den in Zukunft festgelegten Vertragscode aufzurufen;
- Schichtvertrag – ID des ersten Vertrags in der VM;
- logger - Ausgabe des VM-Fehlerprotokolls.
Blockstruktur
Eine virtuelle Maschine ist ein Baum, der aus Blocktyp-Objekten besteht.
Ein Block ist eine unabhängige Einheit, die einige Bytecodes enthält. Einfach ausgedrückt ist alles, was Sie in der Sprache in die geschweiften Klammern ({}
) setzen, ein Block.
Der folgende Code würde beispielsweise einen Block mit Funktionen erstellen. Dieser Block enthält auch einen weiteren Block mit einer if-Anweisung, der wiederum einen Block mit einer while-Anweisung enthält.
Der Block ist im Speicher als Struktur wie unten organisiert.
Eine Blockstruktur besteht aus folgenden Elementen:
- Objekte - eine Abbildung interner Objekte des Zeigertyps ObjInfo. Wenn der Block beispielsweise eine Variable enthält, können Sie anhand ihres Namens Informationen darüber erhalten.
- Typ - der Typ des Blocks. Bei einem Funktionsblock ist sein Typ ObjFunc; für einen Vertragsblock ist sein Typ ObjContract;
- Eigentümer – eine Struktur vom Zeigertyp Eigentümerinfo. Diese Struktur enthält Informationen über den Eigentümer des kompilierten Vertrags, der während der Vertragserstellung angegeben oder aus der Tabelle Verträge erhalten wird;
- Info - enthält Informationen über das Objekt, die vom Blocktyp abhängen;
- Parent – ein Zeiger auf den Elternblock;
- Vars - ein Array, das die Typen der aktuellen Blockvariablen enthält;
- Code - der Bytecode des Blocks selbst, der ausgeführt wird, wenn die Kontrollrechte an den Block übergeben werden, zum Beispiel Funktionsaufrufe oder Schleifenkörper;
- Children - ein Array mit Unterblöcken, wie z. B. Funktionsverschachtelung, Schleifen, bedingte Operatoren.
ObjInfo-Struktur
Die ObjInfo-Struktur enthält Informationen über interne Objekte.
Die ObjInfo-Struktur hat die folgenden Elemente:
- Typ ist der Objekttyp, der einen der folgenden Werte hat:
- ObjContract – Vertrag;
- ObjFunc - Funktion;
- ObjExtFunc - externe Golang-Funktion;
- ObjVar - Variable;
- ObjExtend - $name-Variable.
- Wert – enthält die Struktur jedes Typs.
ContractInfo-Struktur
Zeigt auf den Typ ObjContract, und das Feld Value enthält eine ContractInfo-Struktur.
Die ContractInfo-Struktur hat die folgenden Elemente:
- ID - Vertrags-ID, die beim Aufruf des Vertrags in der Blockchain angezeigt wird;
- Name - Vertragsname;
- Eigentümer - andere Informationen zum Vertrag;
- Verwendet - Karte der aufgerufenen Vertragsnamen;
- Tx – ein Datenarray, das im Datenabschnitt des Vertrags beschrieben wird.
FieldInfo-Struktur
Die FieldInfo-Struktur wird in der ContractInfo-Struktur verwendet und beschreibt Elemente im Datenabschnitt eines Vertrags.
Die FieldInfo-Struktur hat die folgenden Elemente:
- Name - Feldname;
- Typ - Feldtyp;
- Original - optionales Feld;
- Tags - zusätzliche Beschriftungen für dieses Feld.
FuncInfo-Struktur
Zeigt auf den ObjFunc-Typ, und das Value-Feld enthält eine FuncInfo-Struktur.
Die FuncInfo-Struktur hat die folgenden Elemente:
- Params - ein Array von Parametertypen;
- Ergebnisse - ein Array zurückgegebener Typen;
- Namen - Abbildung von Daten für Tail-Funktionen, zum Beispiel
DBFind().Columns ()
; - Variadic - wahr, wenn die Funktion eine variable Anzahl von Parametern haben kann;
- ID - Funktions-ID.
FuncName-Struktur
Die Struktur FuncName wird für FuncInfo verwendet und beschreibt die Daten einer Tail-Funktion.
Die FuncName-Struktur hat die folgenden Elemente:
- Params - ein Array von Parametertypen;
- Offset - das Array von Offsets für diese Variablen. Tatsächlich können die Werte aller Parameter in einer Funktion mit dem Punkt . initialisiert werden;
- Variadic - true, wenn die Tail-Funktion eine variable Anzahl von Parametern haben kann.
ExtFuncInfo-Struktur
Zeigt auf den ObjExtFunc-Typ, und das Value-Feld enthält eine ExtFuncInfo-Struktur. Es wird verwendet, um Golang-Funktionen zu beschreiben.
Die ExtFuncInfo-Struktur hat die folgenden Elemente:
- Die Parameter Name, Params, Results haben die gleiche Struktur wie FuncInfo;
- Auto - ein Array von Variablen. Wird gegebenenfalls als zusätzlicher Parameter an die Funktion übergeben. Beispielsweise eine Variable vom Typ SmartContract sc;
- Func - Golang-Funktionen.
VarInfo-Struktur
Zeigt auf den Typ ObjVar, und das Feld Value enthält eine VarInfo-Struktur.
Die VarInfo-Struktur hat die folgenden Elemente:
- Obj - Informationen über Typ und Wert der Variablen;
- Eigentümer - Zeiger auf den Eigentümerblock.
ObjExtend-Wert
Zeigt auf den Typ ObjExtend, und das Feld Value enthält eine Zeichenfolge, die den Namen der Variablen oder Funktion enthält.
Befehle für virtuelle Maschinen
ByteCode-Struktur
Ein Bytecode ist eine Folge von Strukturen vom Typ ByteCode.
Diese Struktur hat die folgenden Felder:
- Cmd - der Bezeichner der Speicherbefehle;
- Wert - enthält den Operanden (Wert).
Im Allgemeinen führen Befehle eine Operation auf dem obersten Element des Stapels aus und schreiben bei Bedarf den Ergebniswert hinein.
Befehlskennungen
Bezeichner der Befehle der virtuellen Maschine sind in der Datei vm/cmds_list.go beschrieben.
- cmdPush – legt einen Wert aus dem Value-Feld auf den Stack. Legen Sie zum Beispiel Zahlen und Linien auf den Stapel;
- cmdVar - Legt den Wert einer Variablen auf den Stack. Value enthält einen Zeiger auf die VarInfo-Struktur und Informationen über die Variable;
- cmdExtend – legt den Wert einer externen Variablen auf den Stack. Wert enthält eine Zeichenfolge mit dem Variablennamen (beginnend mit $);
- cmdCallExtend – Aufruf einer externen Funktion (beginnend mit $). Die Parameter der Funktion werden aus dem Stapel abgerufen und die Ergebnisse auf dem Stapel abgelegt. Wert enthält einen Funktionsnamen (beginnend mit $);
- cmdPushStr – legt den String in Value auf den Stack;
- cmdCall - ruft die Funktion der virtuellen Maschine auf. Wert enthält eine ObjInfo-Struktur. Dieser Befehl gilt für die Golang-Funktion ObjExtFunc und die Needle-Funktion ObjFunc. Wenn eine Funktion aufgerufen wird, werden ihre Parameter vom Stapel abgerufen und die Ergebniswerte werden auf dem Stapel abgelegt;
- cmdCallVari - Ähnlich wie der Befehl cmdCall ruft er die Funktion der virtuellen Maschine auf. Dieser Befehl wird verwendet, um eine Funktion mit einer variablen Anzahl von Parametern aufzurufen;
- cmdReturn - wird verwendet, um die Funktion zu verlassen. Die Rückgabewerte werden auf den Stack gelegt und das Value-Feld wird nicht verwendet;
- cmdIf – übergibt die Kontrolle an den Bytecode in der Struktur block, der im Feld Wert übergeben wird. Das Steuerelement wird nur dann auf den Stack übertragen, wenn das oberste Element des Stacks von der valueToBool-Funktion aufgerufen und
true
zurückgegeben wird. Andernfalls wird die Steuerung an den nächsten Befehl übergeben; - cmdElse - dieser Befehl funktioniert auf die gleiche Weise wie cmdIf, aber nur wenn das oberste Element des Stacks von der valueToBool-Funktion aufgerufen und
false
zurückgegeben wird, wird die Steuerung an die übertragen angegebener Block; - cmdAssignVar – erhält eine Liste von Variablen des Typs VarInfo von Value. Diese Variablen verwenden den Befehl cmdAssign, um den Wert abzurufen;
- cmdAssign – weist den Wert im Stack der Variablen zu, die durch den Befehl cmdAssignVar erhalten wurde;
- cmdLabel - definiert ein Label, wenn die Steuerung während der While-Schleife zurückgegeben wird;
- cmdContinue - Dieser Befehl überträgt die Steuerung an das Label cmdLabel. Beim Ausführen einer neuen Iteration der Schleife wird Value nicht verwendet;
- cmdWhile – Verwenden Sie valueToBool, um das oberste Element des Stapels zu überprüfen. Wenn dieser Wert „true“ ist, wird die Struktur block aus dem Wertefeld aufgerufen;
- cmdBreak - beendet die Schleife;
- cmdIndex – legt den Wert in der Map oder im Array nach Index in den Stack, ohne Value zu verwenden. Beispiel:
(map | array) (index value) => (map | array [index value])
; - cmdSetIndex – weist den Wert des obersten Elements des Stapels den Elementen der Karte oder des Arrays zu, ohne Value zu verwenden. Beispiel:
(map | array) (index value) (value) => (map | array)
; - cmdFuncName - fügt Parameter hinzu, die mit sequentiellen Beschreibungen geteilt durch Punkt übergeben werden. Beispiel:
func name => Fun (...) .Name (...)
; - cmdUnwrapArr - definiert ein boolesches Flag, wenn das oberste Element des Stapels ein Array ist;
- cmdMapInit – initialisiert den Wert von map;
- cmdArrayInit – initialisiert den Wert des Arrays;
- cmdError - Dieser Befehl wird erstellt, wenn ein Vertrag oder eine Funktion mit einem angegebenen
error, warning, info
beendet wird.
Stack-Operationsbefehle
Hinweis
In der aktuellen Version ist die automatische Typkonvertierung für diese Befehle nicht vollständig anwendbar. Zum Beispiel,
string + float | int | decimal => float | int | decimal, float + int | str => float, but int + string => runtime error
.
Das Folgende sind Befehle für die direkte Stack-Verarbeitung. Das Feld Wert wird in diesen Befehlen nicht verwendet.
- cmdNot - logische Negation.
(val) => (!ValueToBool(val))
; - cmdSign - Vorzeichenwechsel.
(val) => (-val)
; - cmdAdd - Ergänzung.
(val1)(val2) => (val1 + val2)
; - cmdSub - Subtraktion.
(val1)(val2) => (val1-val2)
; - cmdMul - Multiplikation.
(val1)(val2) => (val1 * val2)
; - cmdDiv - Division.
(val1)(val2) => (val1 / val2)
; - cmdAnd - logisches UND.
(val1)(val2) => (valueToBool(val1) && valueToBool(val2))
; - cmdOr - logisches ODER.
(val1)(val2) => (valueToBool(val1) || valueToBool(val2))
; - cmdEqual - Gleichheitsvergleich, bool wird zurückgegeben.
(val1)(val2) => (val1 == val2)
; - cmdNotEq - Ungleichheitsvergleich, bool wird zurückgegeben.
(val1)(val2) => (val1 != val2)
; - cmdLess - Kleiner-als-Vergleich, bool wird zurückgegeben.
(val1)(val2) => (val1 <val2)
; - cmdNotLess - Größer-gleich-Vergleich, bool wird zurückgegeben.
(val1)(val2) => (val1 >= val2)
; - cmdGreat - Größer-als-Vergleich, bool wird zurückgegeben.
(val1)(val2) => (val1> val2)
; - cmdNotGreat - Kleiner-gleich-Vergleich, bool wird zurückgegeben.
(val1)(val2) => (val1 <= val2)
.
Laufzeitstruktur
Die Ausführung von Bytecodes wirkt sich nicht auf die virtuelle Maschine aus. Beispielsweise können verschiedene Funktionen und Verträge gleichzeitig in einer einzigen virtuellen Maschine ausgeführt werden. Die Runtime-Struktur wird verwendet, um Funktionen und Verträge sowie beliebige Ausdrücke und Bytecode auszuführen.
- stack - der Stack zum Ausführen des Bytecodes;
- blocks - Stack für Blockaufrufe;
- vars - Stapel von Variablen. Seine Variable wird dem Stapel von Variablen hinzugefügt, wenn der Bytecode im Block aufgerufen wird. Nach dem Verlassen des Blocks kehrt die Größe des Variablenstapels zum vorherigen Wert zurück;
- extend - ein Zeiger zum Abbilden mit Werten externer Variablen (
$name
); - vm - ein Zeiger einer virtuellen Maschine;
- cost - Kraftstoffeinheit der resultierenden Ausführungskosten;
- err - Fehler während der Ausführung aufgetreten.
blockStack-Struktur
Die blockStack-Struktur wird in der Runtime-Struktur verwendet.
- Block – ein Zeiger auf den ausgeführten Block;
- Offset – der Offset des letzten ausgeführten Befehls im Bytecode des angegebenen Blocks.
RunCode-Funktion
Bytecodes werden in der Funktion RunCode ausgeführt. Es enthält eine Schleife, die die entsprechende Operation für jeden Bytecode-Befehl durchführt. Vor der Verarbeitung eines Bytecodes müssen die erforderlichen Daten initialisiert werden.
Neue Blöcke werden zu anderen Blöcken hinzugefügt.
Rufen Sie als Nächstes die Informationen zu den relevanten Parametern der Tail-Funktion ab. Diese Parameter sind im letzten Element des Stapels enthalten.
Anschließend müssen alle im aktuellen Block definierten Variablen mit ihren Anfangswerten initialisiert werden.
Da Variablen in der Funktion auch Variablen sind, müssen wir sie vom letzten Element des Stacks in der von der Funktion selbst beschriebenen Reihenfolge abrufen.
Lokale Variablen mit ihren Anfangswerten initialisieren.
Aktualisieren Sie als Nächstes die Werte der variablen Parameter, die in der Tail-Funktion übergeben werden.
Wenn übergebene variable Parameter zu einer variablen Anzahl von Parametern gehören, werden diese Parameter zu einem Array von Variablen kombiniert.
Danach müssen wir nur noch Werte löschen, die von der Spitze des Stacks als Funktionsparameter übergeben wurden, wodurch der Stack verschoben wird. Wir haben ihre Werte in ein Variablenarray kopiert.
Wenn eine Bytecode-Befehlsschleife beendet ist, müssen wir den Stack korrekt leeren.
Löschen Sie den aktuellen Block aus dem Blockstapel.
Wenn eine bereits ausgeführte Funktion erfolgreich beendet wird, fügen wir den Rückgabewert am Ende des vorherigen Stacks hinzu.
Wie Sie sehen können, stellen wir den Stack-Status nicht wieder her und beenden die Funktion unverändert, wenn wir die Funktion nicht ausführen. Der Grund ist, dass Schleifen und bedingte Strukturen, die in der Funktion ausgeführt wurden, ebenfalls Bytecode-Blöcke sind.
Weitere Funktionen für Operationen mit VM
Mit der Funktion NewVM können Sie eine virtuelle Maschine erstellen. Jeder virtuellen Maschine werden über die Funktion Extend vier Funktionen hinzugefügt, z. B. ExecContract, MemoryUsage, CallContract und Settings.
Wir durchlaufen alle übergebenen Objekte und betrachten nur die Funktionen.
Wir füllen die ExtFuncInfo-Struktur gemäß den über die Funktion erhaltenen Informationen und fügen ihre Struktur namentlich der Top-Level-Map Objects hinzu.
Die ExtFuncInfo-Struktur hat ein Auto-Parameter-Array. Normalerweise ist der erste Parameter sc *SmartContract
oder rt *Runtime
, wir können sie nicht von der Sprache Needle übergeben, da sie für uns notwendig sind, um einige Golang-Funktionen auszuführen. Daher legen wir fest, dass diese Variablen automatisch verwendet werden, wenn diese Funktionen aufgerufen werden. In diesem Fall ist der erste Parameter der obigen vier Funktionen rt *Runtime
.
Informationen zur Parametrierung.
Und die Typen der Rückgabewerte.
Fügt eine Funktion zu den Objekten des Stammverzeichnisses hinzu, damit der Compiler sie später finden kann, wenn er den Vertrag verwendet.
Compiler
Funktionen in der Datei compile.go sind für das Kompilieren des Token-Arrays verantwortlich, das vom lexikalischen Analysator erhalten wird. Die Zusammenstellung kann bedingt in zwei Ebenen unterteilt werden. Auf der obersten Ebene beschäftigen wir uns mit Funktionen, Verträgen, Codeblöcken, Bedingungs- und Schleifenanweisungen, Variablendefinitionen und so weiter. Auf der unteren Ebene kompilieren wir Ausdrücke in Codeblöcke oder Bedingungen in Schleifen und bedingte Anweisungen.
Zunächst beginnen wir mit der einfachen unteren Ebene. In der Funktion compileEval können Ausdrücke in Bytecode umgewandelt werden. Da wir eine virtuelle Maschine mit einem Stack verwenden, ist es notwendig, gewöhnliche Infix-Record-Ausdrücke in Postfix-Notation oder umgekehrte polnische Notation umzuwandeln. Zum Beispiel wandeln wir „1+2“ in „12+“ um und legen „1“ und „2“ auf den Stack. Dann wenden wir die Additionsoperation auf die letzten beiden Elemente im Stack an und schreiben das Ergebnis in den Stack. Sie finden diesen Konvertierungs- (opens new window) Algorithmus im Internet.
Die globale Variable „opers = map [uint32] operPrior“ enthält die Priorität der Operationen, die für die Konvertierung in die inverse polnische Notation erforderlich sind.
Die folgenden Variablen werden am Anfang der Funktion compileEval definiert:
- Puffer - temporärer Puffer für Bytecode-Befehle;
- Bytecode - letzter Puffer von Bytecode-Befehlen;
- parcount - temporärer Puffer, der zum Berechnen von Parametern beim Aufrufen einer Funktion verwendet wird;
- setIndex - Variablen im Arbeitsprozess werden auf true gesetzt, wenn wir Map- oder Array-Elemente zuweisen. Beispiel:
a["my"] = 10
. In diesem Fall müssen wir den angegebenen Befehl cmdSetIndex verwenden.
Wir erhalten ein Token in einer Schleife und verarbeiten es entsprechend. Beispielsweise wird die Ausdruckstrennung gestoppt, wenn geschweiften Klammern gefunden werden. Beim Verschieben der Zeichenfolge prüfen wir, ob die vorherige Anweisung eine Operation ist und ob sie innerhalb der Klammern steht, andernfalls wird der Ausdruck beendet und analysiert.
Im Allgemeinen entspricht der Algorithmus selbst einem Algorithmus zum Umwandeln in die inverse polnische Notation. Unter Berücksichtigung des Aufrufs notwendiger Verträge, Funktionen und Indizes sowie anderer Dinge, die beim Parsing nicht vorkommen, und Optionen zum Parsing von Token vom Typ lexIdent werden dann Variablen, Funktionen oder Verträge mit diesem Namen überprüft. Wenn nichts gefunden wird und es sich nicht um einen Funktions- oder Vertragsaufruf handelt, wird ein Fehler angezeigt.
Wir können auf eine solche Situation stoßen, und der Vertragsaufruf wird später beschrieben. Wenn in diesem Beispiel keine Funktionen oder Variablen mit demselben Namen gefunden werden, halten wir es für notwendig, einen Vertrag aufzurufen. In dieser kompilierten Sprache gibt es keinen Unterschied zwischen Verträgen und Funktionsaufrufen. Aber wir müssen den Vertrag über die Funktion ExecContract aufrufen, die im Bytecode verwendet wird.
Die Anzahl der bisherigen Variablen erfassen wir in count
, die zusammen mit der Anzahl der Funktionsparameter auch auf den Stack geschrieben werden. Bei jeder weiteren Erkennung von Parametern müssen wir diese Zahl nur am letzten Element des Stapels um eine Einheit erhöhen.
Wir haben eine Liste mit aufgerufenen Parametern für Verträge verwendet, dann müssen wir den Fall markieren, in dem der Vertrag aufgerufen wird. Wenn der Vertrag ohne Parameter aufgerufen wird, müssen wir zwei leere Parameter hinzufügen, um ExecContract aufzurufen, um mindestens zwei Parameter zu erhalten.
Wenn wir sehen, dass als nächstes eine eckige Klammer steht, fügen wir den Befehl cmdIndex hinzu, um den Wert durch den Index zu erhalten.
Die Funktion CompileBlock kann Objektbäume und ausdrucksunabhängige Bytecodes generieren. Der Kompilierungsprozess basiert auf einer endlichen Zustandsmaschine, genau wie ein lexikalischer Analysator, aber mit den folgenden Unterschieden. Erstens verwenden wir keine Symbole, sondern Tokens; zweitens werden wir gleich die states-Variablen in allen Zuständen und Übergängen beschreiben. Es stellt ein Array von Objekten dar, die nach Tokentyp indiziert sind. Jedes Token hat eine Struktur von compileState, und ein neuer Status wird in NewState angegeben. Wenn klar ist, welche Struktur wir aufgelöst haben, können wir die Funktion des Handlers im Feld Func angeben.
Betrachten wir den Hauptzustand als Beispiel.
Wenn wir auf einen Zeilenumbruch oder einen Kommentar stoßen, bleiben wir im selben Zustand. Wenn wir auf das Schlüsselwort contract stoßen, ändern wir den Status in stateContract und beginnen mit dem Parsen der Struktur. Wenn wir auf das Schlüsselwort func stoßen, ändern wir den Status in stateFunc. Wenn andere Token empfangen werden, wird die Funktion, die einen Fehler generiert, aufgerufen.
Angenommen, wir sind auf das Schlüsselwort func gestoßen und haben den Status in stateFunc geändert. Da der Funktionsname auf das Schlüsselwort func folgen muss, behalten wir denselben Zustand bei, wenn wir den Funktionsnamen ändern. Für alle anderen Token werden wir entsprechende Fehler generieren. Wenn wir den Funktionsnamen in der Token-ID erhalten, gehen wir zum stateFParams-Zustand, wo wir die Parameter der Funktion erhalten können.
Gleichzeitig mit den obigen Operationen rufen wir die Funktion fNameBlock auf. Es sollte beachtet werden, dass die Blockstruktur mit der statePush-Markierung erstellt wird, wo wir sie aus dem Puffer holen und mit den Daten füllen, die wir benötigen. Die fNameBlock-Funktion eignet sich für Verträge und Funktionen (einschließlich der darin verschachtelten). Er füllt das Info-Feld mit der entsprechenden Struktur und schreibt sich in die Objects des übergeordneten Blocks. Auf diese Weise können wir die Funktion oder den Vertrag mit dem angegebenen Namen aufrufen. Ebenso erstellen wir entsprechende Funktionen für alle Zustände und Variablen. Diese Funktionen sind normalerweise sehr klein und führen einige Aufgaben beim Erstellen des Baums der virtuellen Maschine aus.
Für die Funktion CompileBlock durchläuft sie einfach alle Tokens und wechselt die Zustände gemäß den in den Zuständen beschriebenen Tokens. Fast alle zusätzlichen Token entsprechen zusätzlichen Programmcodes.
- statePush – fügt das Objekt Block zum Objektbaum hinzu;
- statePop - wird verwendet, wenn der Block mit einer schließenden geschweiften Klammer endet;
- stateStay - Sie müssen die aktuelle Markierung beibehalten, wenn Sie in einen neuen Status wechseln;
- stateToBlock - Übergang in den Zustand stateBlock zur Verarbeitung von while und if. Nach der Verarbeitung von Ausdrücken müssen Sie Blöcke innerhalb der geschweiften Klammern verarbeiten;
- stateToBody - Übergang in den Zustand stateBody;
- stateFork - speichert die markierte Position. Wenn der Ausdruck mit einem Bezeichner oder einem Namen mit
$
beginnt, können wir Funktionsaufrufe oder Zuweisungen vornehmen; - stateToFork – wird verwendet, um das in stateFork gespeicherte Token abzurufen, das an die Prozessfunktion übergeben wird;
- stateLabel – wird zum Einfügen von cmdLabel-Befehlen verwendet. während die Struktur dieses Flag erfordert;
- stateMustEval – prüft die Verfügbarkeit von bedingten Ausdrücken am Anfang von if- und while-Strukturen.
Neben der Funktion CompileBlock ist auch die Funktion FlushBlock zu nennen. Das Problem besteht jedoch darin, dass der Blockbaum unabhängig von vorhandenen virtuellen Maschinen erstellt wird. Genauer gesagt erhalten wir Informationen über Funktionen und Verträge, die in einer virtuellen Maschine vorhanden sind, aber wir sammeln die kompilierten Blöcke in einem separaten Baum. Andernfalls, wenn während der Kompilierung ein Fehler auftritt, müssen wir die virtuelle Maschine auf den vorherigen Zustand zurücksetzen. Daher gehen wir separat zum Kompilierungsbaum, aber nachdem die Kompilierung erfolgreich ist, muss die Funktion FlushContract aufgerufen werden. Diese Funktion fügt den fertigen Blockbaum zur aktuellen virtuellen Maschine hinzu. Die Kompilierungsphase ist nun abgeschlossen.
Lexikalischer Analysator
Der lexikalische Analysator verarbeitet eingehende Zeichenfolgen und bildet eine Folge von Token der folgenden Typen:
- lexSys - Systemtoken, zum Beispiel:
{}, [], (), ,, .
usw.; - lexOper - Vorgangstoken, zum Beispiel:
+, -, /, \, *
; - lexNumber - Zahl;
- lexident - Kennung;
- lexNewline - Zeilenumbruchzeichen;
- lexString - Zeichenkette;
- lexComment - Kommentar;
- lexKeyword - Schlüsselwort;
- lexType - Typ;
- lexExtend - Verweis auf externe Variablen oder Funktionen, zum Beispiel:
$myname
.
In der aktuellen Version wird zunächst mit Hilfe der Datei script/lextable/lextable.go eine Konvertierungstabelle (finite state machine) zum Parsen der Tokens aufgebaut, die in die Datei lex_table.go geschrieben wird. Im Allgemeinen können Sie die ursprünglich von der Datei generierte Konvertierungstabelle loswerden und direkt beim Start eine Konvertierungstabelle im Speicher erstellen (init()
). Die lexikalische Analyse selbst findet in der lexParser-Funktion in der Datei lex.go statt.
lextable/lextable.go
Hier definieren wir das zu betreibende Alphabet und beschreiben, wie die endliche Zustandsmaschine basierend auf dem nächsten empfangenen Symbol von einem Zustand in einen anderen wechselt.
states ist ein JSON-Objekt, das eine Liste von Zuständen enthält.
Mit Ausnahme bestimmter Symbole steht „d“ für alle Symbole, die nicht im Staat angegeben sind.
„n“ steht für 0x0a, „s“ steht für Leerzeichen, „q“ steht für Backquote, „Q“ steht für doppeltes Anführungszeichen, „r“ steht für Zeichen >= 128, „a“ steht für AZ und az und „ 1` steht für 1-9.
Die Namen dieser Zustände sind Schlüssel, und die möglichen Werte sind im Wertobjekt aufgelistet. Dann gibt es einen neuen Zustand, um Übergänge für jede Gruppe vorzunehmen. Dann gibt es noch den Namen des Tokens. Wenn wir zum Anfangszustand zurückkehren müssen, ist der dritte Parameter das Service-Token, das angibt, wie mit dem aktuellen Symbol umgegangen werden soll.
Zum Beispiel haben wir den Hauptzustand und die eingehenden Zeichen /
, "/": ["solidus", "", "push next"]
,
- push - gibt den Befehl, sich daran zu erinnern, dass es sich in einem separaten Stack befindet ;
- nächster - geht zum nächsten Zeichen, und gleichzeitig ändern wir den Status auf solidus. Ruft danach das nächste Zeichen ab und überprüft den Status von solidus.
Wenn das nächste Zeichen /
oder /*
hat, gehen wir zum Kommentarstatus Kommentar, weil sie mit //
oder /*
beginnen. Offensichtlich hat jeder Kommentar danach einen anderen Zustand, weil sie mit einem anderen Symbol enden.
Wenn das nächste Zeichen nicht /
und *
ist, dann zeichnen wir alles im Stack als Tags vom Typ lexOper auf, leeren den Stack und kehren zum Hauptzustand zurück.
Das folgende Modul konvertiert den Zustandsbaum in ein numerisches Array und schreibt es in die Datei lex_table.go.
In der ersten Schleife:
Wir bilden ein Alphabet gültiger Symbole.
Darüber hinaus versehen wir in state2int jeden Zustand mit einer eigenen Sequenzkennung.
Wenn wir alle Zustände und jede Menge in einem Zustand und jedes Symbol in einer Menge durchlaufen, schreiben wir eine Drei-Byte-Zahl [neue Zustandskennung (0 = Haupt)] + [Tokentyp (0-kein Token)] + [Token] .
Die Zweidimensionalität des Arrays table besteht darin, dass es in Zustände und 34 Eingabesymbole aus dem Array alphabet unterteilt ist, die in derselben Reihenfolge angeordnet sind.
Wir befinden uns im Hauptzustand in der Nullzeile der Tabelle. Nehmen Sie das erste Zeichen, finden Sie seinen Index im Array alphabet und erhalten Sie den Wert aus der Spalte mit dem angegebenen Index. Ausgehend vom empfangenen Wert erhalten wir den Token im Low-Byte. Wenn die Analyse abgeschlossen ist, zeigt das zweite Byte den Typ des empfangenen Tokens an. Im dritten Byte erhalten wir den Index des nächsten neuen Zustands.
All dies wird ausführlicher in der Funktion lexParser in lex.go beschrieben.
Wenn Sie einige neue Zeichen hinzufügen möchten, müssen Sie sie dem Array alphabet hinzufügen und die Menge der Konstante AlphaSize erhöhen. Wenn Sie eine neue Symbolkombination hinzufügen möchten, sollte diese ähnlich wie bei den bestehenden Optionen im Status beschrieben werden. Führen Sie nach dem obigen Vorgang die Datei lextable.go aus, um die Datei lex_table.go zu aktualisieren.
lex.go
Die lexParser-Funktion generiert direkt eine lexikalische Analyse und gibt basierend auf eingehenden Zeichenfolgen ein Array empfangener Tags zurück. Lassen Sie uns die Struktur von Token analysieren.
- Type - Token-Typ. Es hat einen der folgenden Werte:
lexSys, lexOper, lexNumber, lexIdent, lexString, lexComment, lexKeyword, lexType, lexExtend
; - Value – Wert des Tokens. Die Art des Werts hängt vom Token-Typ ab. Lassen Sie uns das genauer analysieren:
- lexSys - enthält Klammern, Kommas usw. In diesem Fall `Type = ch << 8 | lexSys“, beziehen Sie sich bitte auf die Konstante „isLPar ... isRBrack“, und ihr Wert ist uint32 Bits;
- lexOper - der Wert stellt eine äquivalente Zeichenfolge in Form von uint32 dar. Siehe die
isNot ... isOr
-Konstanten; - lexNumber - Zahlen werden als int64 oder float64 gespeichert. Wenn die Zahl einen Dezimalpunkt hat, ist sie Float64;
- lexIdent - Identifikatoren werden als String gespeichert;
- lexNewLine - Zeilenumbruchzeichen. Wird auch zur Berechnung der Reihen- und Tokenposition verwendet;
- lexString - Zeilen werden als String gespeichert;
- lexComment - Kommentare werden als String gespeichert;
- lexKeyword - für Schlüsselwörter werden nur die entsprechenden Indizes gespeichert, siehe
keyContract ... keyTail
-Konstante. In diesem Fall Type = KeyID << 8 | lexKeyword
. Außerdem ist zu beachten, dass die Schlüsselwörter „true, false, nil“ sofort in Token vom Typ „lexNumber“ umgewandelt und die entsprechenden Typen „bool“ und „interface {}“ verwendet werden; - lexType – dieser Wert enthält den entsprechenden „reflect.Type“-Typwert;
- lexExtend – Bezeichner, die mit einem
$
beginnen. Diese Variablen und Funktionen werden von außen übergeben und sind daher speziellen Arten von Token zugeordnet. Dieser Wert enthält den Namen als String ohne $ am Anfang.
- Line - die Zeile, in der das Token gefunden wird;
- Column - Inline-Position des Tokens.
Lassen Sie uns die Funktion lexParser im Detail analysieren. Die todo-Funktion schlägt den Symbolindex im Alphabet basierend auf dem aktuellen Status und dem eingehenden Symbol nach und erhält einen neuen Status, Token-Identifikator (falls vorhanden) und andere Token aus der Umwandlungstabelle. Das Parsing selbst beinhaltet das Aufrufen der todo-Funktion der Reihe nach für jedes nächste Zeichen und das Wechseln in einen neuen Zustand. Sobald das Tag empfangen wurde, erstellen wir das entsprechende Token in den Ausgabekriterien und setzen den Parsing-Prozess fort. Es sollte beachtet werden, dass wir während des Parsing-Prozesses die Token-Symbole nicht in einem separaten Stack oder Array akkumulieren, weil wir nur den Offset des Starts des Tokens speichern. Nachdem wir das Token erhalten haben, verschieben wir den Offset des nächsten Tokens an die aktuelle Parsing-Position.
Es bleibt nur noch, die beim Parsing verwendeten lexikalischen Status-Token zu überprüfen:
- lexfPush - dieses Token bedeutet, dass wir beginnen, Symbole in einem neuen Token zu sammeln;
- lexfNext - das Zeichen muss dem aktuellen Token hinzugefügt werden;
- lexfPop - Der Empfang des Tokens ist abgeschlossen. Normalerweise haben wir mit diesem Flag den Bezeichnertyp des geparsten Tokens;
- lexfSkip - Dieses Token wird verwendet, um Zeichen vom Parsen auszuschließen. Beispielsweise sind die Kontrollschrägstriche in der Zeichenfolge \n \r ". Sie werden während der lexikalischen Analysephase automatisch ersetzt.
Needle Sprache
Lexemes
Der Quellcode eines Programms muss in UTF-8-Kodierung vorliegen.
Die folgenden lexikalischen Typen werden verarbeitet:
- Schlüsselwörter -
action, break, conditions, Continue, Contract, data, else, error, false, func, If, info, nil, return, settings, true, var, warning, while
; - Zahl - nur Dezimalzahlen werden akzeptiert. Es gibt zwei Grundtypen: int und float. Wenn die Zahl einen Dezimalpunkt hat, wird sie zu einem Float float. Der Typ int entspricht in Golang int64, während der Typ float in Golang float64 entspricht.
- String - Der String kann in doppelte Anführungszeichen
("ein String")
oder Backquotes (\`ein String\`)
eingeschlossen werden. Beide Arten von Zeichenfolgen können Zeilenumbruchzeichen enthalten. Zeichenfolgen in doppelten Anführungszeichen können doppelte Anführungszeichen, Zeilenumbruchzeichen und mit Schrägstrichen maskierte Wagenrückläufe enthalten. Beispiel: "Dies ist eine \"erste Zeichenfolge\".\r\nDies ist eine zweite Zeichenfolge."
. - Kommentar - Es gibt zwei Arten von Kommentaren. Einzeilige Kommentare verwenden zwei Schrägstriche (//). Beispiel: // Dies ist ein einzeiliger Kommentar. Mehrzeilige Kommentare verwenden Schrägstriche und Sternchen und können sich über mehrere Zeilen erstrecken. Beispiel:
/* Dies ist ein mehrzeiliger Kommentar */
. - Bezeichner - die Namen von Variablen und Funktionen, die aus Buchstaben a-z und A-Z, UTF-8-Symbolen, Zahlen und Unterstrichen bestehen. Der Name kann mit einem Buchstaben, Unterstrich,
@
oder $
beginnen. Der Name, der mit $
beginnt, ist der Name der Variablen, die im Datenabschnitt definiert ist. Der mit $
beginnende Name kann auch verwendet werden, um globale Variablen im Bereich von Bedingungen und Aktionsabschnitten zu definieren. Ökosystemverträge können über das Symbol @
aufgerufen werden. Zum Beispiel: @1NewTable(...)
.
Typen
Neben den Nadeltypen sind entsprechende Golang-Typen angegeben.
- bool - bool, standardmäßig false;
- bytes - []byte{}, standardmäßig ein leeres Byte-Array;
- int - standardmäßig int64, 0;
- address - uint64, standardmäßig 0;
- array - []interface{}, standardmäßig ein leeres Array;
- map - map[string]interface{}, standardmäßig ein leeres Objekt-Array;
- money - Dezimalzahl. Dezimal, standardmäßig 0;
- float - float64, standardmäßig 0;
- string - String, standardmäßig ein leerer String;
- file - map[string]interface{}, standardmäßig ein leeres Objekt-Array.
Diese Variablentypen werden mit dem Schlüsselwort
var
definiert. Beispiel: var var1, var2 int
. Bei dieser Definition wird einer Variablen ein Standardwert nach Typ zugewiesen.
Alle Variablenwerte sind vom Typ interface{} und werden dann den erforderlichen Golang-Typen zugewiesen. Daher sind beispielsweise Array- und Map-Typen Golang-Typen []interface{} und map[string]interface{}. Beide Arten von Arrays können Elemente beliebigen Typs enthalten.
Ausdrücke
Ein Ausdruck kann arithmetische Operationen, logische Operationen und Funktionsaufrufe enthalten. Alle Ausdrücke werden von links nach rechts nach Priorität der Operatoren ausgewertet. Bei gleicher Priorität werden Operatoren von links nach rechts ausgewertet.
Priorität der Operationen von hoch nach niedrig:
- Funktionsaufruf und Klammern - Beim Aufruf einer Funktion werden übergebene Parameter von links nach rechts berechnet;
- Unäre Operation - logische Negation
!
und arithmetischer Vorzeichenwechsel -
; - Multiplikation und Division - arithmetische Multiplikation
*
und Division /
; - Addition und Subtraktion - arithmetische Addition
+
und Subtraktion -
; - Logischer Vergleich -
>=>> >=
; - Logische Gleichheit und Ungleichheit -
== !=
; - Logisches UND -
&&
; - Logisches ODER -
||
.
Bei der Auswertung von logischem AND und OR werden in jedem Fall beide Seiten des Ausdrucks ausgewertet.
Needle hat keine Typprüfung während der Kompilierung. Beim Auswerten von Operanden wird versucht, den Typ in einen komplexeren Typ umzuwandeln. Die Art der Komplexitätsreihenfolge kann wie folgt sein: string, int, float, money
. Nur ein Teil der Typkonvertierungen ist implementiert. Der Zeichenfolgentyp unterstützt Additionsoperationen, und das Ergebnis ist eine Zeichenfolgenverkettung. Beispiel: string + string = string, money-int = money, int * float = float
.
Bei Funktionen wird während der Ausführung eine Typprüfung für die Typen string
und int
durchgeführt.
Die Typen array und map können per Index adressiert werden. Beim Typ array muss als Index der Wert int angegeben werden. Für den Typ Map muss eine Variable oder ein String-Wert angegeben werden. Wenn Sie einem Array-Element einen Wert zuweisen, dessen Index größer als der aktuelle maximale Index ist, wird dem Array ein leeres Element hinzugefügt. Der Anfangswert dieser Elemente ist nil. Zum Beispiel: .. Code:
In Ausdrücken mit bedingten logischen Werten (z. B. if, while, &&, ||, !
) wird der Typ automatisch in einen logischen Wert konvertiert. Wenn der Typ nicht der Standardwert ist, ist er wahr.
Zielfernrohr
Klammern geben einen Block an, der lokale Bereichsvariablen enthalten kann. Standardmäßig erstreckt sich der Geltungsbereich einer Variablen auf ihre eigenen Blöcke und alle verschachtelten Blöcke. In einem Block können Sie eine neue Variable mit dem Namen einer vorhandenen Variablen definieren. In diesem Fall sind jedoch externe Variablen mit demselben Namen nicht mehr verfügbar.
Vertragsabwicklung
Beim Aufruf eines Contracts müssen ihm in data definierte Parameter übergeben werden. Vor Ausführung eines Auftrags erhält die virtuelle Maschine diese Parameter und weist sie den entsprechenden Variablen ($Param) zu. Dann werden die vordefinierten Funktionen conditions und action aufgerufen.
Fehler, die während der Vertragsausführung auftreten, können in zwei Arten unterteilt werden: Formularfehler und Umgebungsfehler. Formularfehler werden mit speziellen Befehlen generiert: error, warning, info
und wenn die eingebaute Funktion err
ungleich nil zurückgibt.
Die Needle-Sprache behandelt keine Ausnahmen. Jeder Fehler wird die Ausführung von Verträgen beenden. Da ein separater Stack und eine separate Struktur zum Speichern von Variablenwerten erstellt werden, wenn ein Vertrag ausgeführt wird, löscht der Golang-Garbage-Collection-Mechanismus diese Daten automatisch, wenn ein Vertrag ausgeführt wird.
In der Informatik ist BNF eine Notationstechnik für kontextfreie Syntax und wird normalerweise verwendet, um die Syntax der beim Rechnen verwendeten Sprache zu beschreiben.