Wir schreiben unsere Programmiersprache, Teil 1: Wir schreiben eine Sprach-VM

Einführung


Guten Tag an alle Habrachitateli!

Vielleicht ist es erwähnenswert, dass das Ziel meiner Arbeit, auf deren Grundlage eine Reihe von Statuen geschrieben werden, darin bestand, selbst ein voll funktionsfähiges YP von 0 zu schaffen und dann mein Wissen, meine Best Practices und meine Erfahrungen mit den Interessierten zu teilen.

Ich werde die Entstehung der Sprache beschreiben , die ich zuvor hier beschrieben habe .

Er interessierte viele und provozierte eine hitzige Diskussion in den Kommentaren. Daher ist das Thema für viele interessant.

Ich denke, es lohnt sich, sofort Informationen über das Projekt zu veröffentlichen:

Site (wird etwas später mit Dokumentation gefüllt).
Repository

Um das Projekt selbst zu berühren und alles in Aktion zu sehen, ist es besser, das Repository herunterzuladen und alles aus dem Ordner bin auszuführen. In der Veröffentlichung habe ich es nicht eilig, die neuesten Versionen der Sprache und Laufzeit hochzuladen, weil Manchmal ist es einfach zu faul für mich, es zu tun.

Ich kann in C / C ++ und Object Pascal codieren. Ich habe das Projekt seitdem auf FPC geschrieben Meiner Meinung nach ist diese Sprache viel einfacher und besser geeignet, um so zu schreiben. Der zweite entscheidende Faktor war, dass FPC eine große Anzahl von Zielplattformen unterstützt und es möglich ist, ein Projekt für die gewünschte Plattform mit einem Minimum an Änderungen neu zu erstellen. Wenn ich Object Pascal aus irgendeinem Grund nicht mag, beeile dich nicht, den Pfosten zu schließen und zu rennen, um Steine ​​auf den Kommentar zu werfen. Diese Sprache ist sehr schön und intuitiv, aber ich werde nicht so viel Code bereitstellen. Genau das, was Sie brauchen.

Vielleicht beginne ich meine Geschichte.

Wir setzen uns Ziele


Zuallererst braucht jedes Projekt seine Ziele und TK, die in Zukunft umgesetzt werden müssen. Es muss im Voraus entschieden werden, welche Art von Sprache erstellt wird, um die primäre VM dafür zu schreiben.

Die wichtigsten Punkte, die die Weiterentwicklung meiner VM bestimmt haben, sind folgende:

  • Dynamisches Tippen und Typgießen. Ich beschloss, ihre Unterstützung in der Entwicklungsphase der VM zu organisieren.
  • Multithreading-Unterstützung. Ich habe dieses Element im Voraus in diese Liste aufgenommen, um die Architektur der VM ordnungsgemäß zu entwerfen und die Unterstützung für Multithreading auf der Kernebene der VM und nicht später mit Krücken zu organisieren.
  • Export externer Methoden. Ohne dies wird die Sprache nutzlos sein. Es sei denn, es in ein Projekt einzubetten.
  • Zusammenstellung der Sprache (in eine einzige abstrakte ausführbare Datei). Teilweise kompiliert oder interpretiert? Viel hängt davon ab.
  • Allgemeine VM-Architektur. Wird der Stack oder das Register unsere VM sein? Ich habe versucht, dies und das umzusetzen. Ich habe eine gestapelte VM zur Unterstützung ausgewählt.
  • Wie sehen Sie die Arbeit mit Variablen, Arrays, Strukturen? Persönlich wollte ich in diesem Moment eine Sprache implementieren, in der fast alles an implizite Zeiger gebunden ist, da ein solcher Ansatz viel Speicherplatz sparen und das Leben des Entwicklers vereinfachen würde. Wenn wir zulassen, dass etwas Großes an Methoden übergeben wird, wird automatisch nur ein Zeiger auf dieses große übertragen.

Daher habe ich die oben genannten Prioritäten ausgewählt und mit der Implementierung der virtuellen Sprachmaschine begonnen. Das ist natürlich seltsam, normalerweise werden zuerst Parser / Übersetzer und dann VMs geschrieben. Nun, ich habe begonnen, das Projekt in dieser Reihenfolge zu entwickeln, und ich werde es in der Reihenfolge, in der ich es entwickelt habe, weiter beschreiben.

Ich muss sofort sagen, dass ich VM so eloquent wie möglich genannt habe - SVM (Stack-based Virtual Machine).

Beginnen wir mit der Implementierung der Variablenklasse


Anfangs habe ich einfach einen Variantentyp verwendet, weil er einfacher und schneller ist. Es war eine Krücke, aber es stützte das Projekt und ermöglichte mir, die erste Version von VM und Sprache schnell zu implementieren. Später setzte ich mich an den Code und schrieb eine Implementierung meiner „Variante“. Im Wesentlichen müssen Sie eine Klasse schreiben, die einen Zeiger auf einen Wert im Speicher speichert. In meiner Implementierung ist dies null/cardinal/int64/double/string/array . Man könnte die Eingabe von Groß- und Kleinschreibung verwenden, aber ich dachte, es wäre besser, die Art und Weise zu implementieren, wie ich sie implementiert habe.

Bevor ich anfing, Klassencode zu schreiben, entschied ich mich, die Direktive {$ H +} sofort in den Modulheader einzufügen, um die Unterstützung von Zeichenfolgen in der zukünftigen Sprache flexibler zu gestalten.
P.S. für diejenigen, die den Unterschied zwischen den Modi H- und H + FPC möglicherweise nicht kennen.

Beim Zusammenstellen von Code im H-Modus werden Zeichenfolgen als Zeichenarray dargestellt. Wenn H + - als Zeiger auf ein Stück Gedächtnis. Im ersten Fall werden die Zeilen zunächst in der Länge festgelegt und standardmäßig auf 256 Zeichen begrenzt. Im zweiten Fall sind die Zeilen dynamisch erweiterbar und es können viel mehr Zeichen eingepfercht werden. Sie arbeiten etwas langsamer, aber funktionaler. Mit H + können Sie Zeichenfolgen auch als Zeichenarray deklarieren, beispielsweise auf folgende Weise:

 var s:string[256]; 
Für den Anfang deklarieren wir Enum als Typ, den wir als bestimmtes Flag verwenden, um den Datentyp durch einen Zeiger zu bestimmen:

 type TSVMType = (svmtNull, svmtWord, svmtInt, svmtReal, svmtStr, svmtArr); 

Als nächstes beschreiben wir die Grundstruktur unseres Variablentyps und einige Methoden:

  TSVMMem = class m_val: pointer; m_type: TSVMType; constructor Create; destructor Destroy; procedure Clear; end; ... constructor TSVMMem.Create; begin m_val := nil; m_type := svmtNull; end; destructor TSVMMem.Destroy; begin Clear; end; procedure TSVMMem.Clear; inline; begin case m_type of svmtNull: { nop }; svmtWord: Dispose(PCardinal(m_val)); svmtInt: Dispose(PInt64(m_val)); svmtReal: Dispose(PDouble(m_val)); svmtStr: Dispose(PString(m_val)); svmtArr: begin SetLength(PMemArray(m_val)^, 0); Dispose(PMemArray(m_val)); end; else Error(reVarInvalidOp); end; end; 

Die Klasse erbt nichts, daher können geerbte Aufrufe im Konstruktor und Destruktor weggelassen werden. Ich werde auf die Inline-Richtlinie achten. Es ist sicher besser, {$ inline on} zum Dateikopf hinzuzufügen. Die aktive Verwendung in VMs erhöhte die Produktivität erheblich (Mb irgendwo um bis zu 15-20%!). Sie teilt dem Compiler mit, dass der Hauptteil der Methode am besten an der Stelle ihres Aufrufs eingebettet ist. Der Ausgabecode wird am Ende etwas größer sein, aber schneller arbeiten. In diesem Fall ist die Verwendung von Inline ratsam.

Ok, zu diesem Zeitpunkt haben wir das Fundament unserer Klasse heruntergespült. Jetzt müssen wir eine Reihe von Setzern und Gettern (Setter & Getter) in unserer Klasse beschreiben.

Die Aufgabe besteht darin, einige Methoden zu schreiben, mit denen Sie die Werte aus unserer Klasse überfüllen und später zurückerhalten können.

Lassen Sie uns zunächst die Zuweisung eines Werts für unsere Klasse herausfinden. Zuerst können Sie einen verallgemeinerten Setter schreiben und dann für einzelne Datentypen:

 procedure TSVMMem.SetV(const value; t:TSVMType); inline; begin if (m_val <> nil) and (m_type = t) then begin case t of svmtWord: PCardinal(m_val)^ := Cardinal(value); svmtInt: PInt64(m_val)^ := Int64(value); svmtReal: PDouble(m_val)^ := Double(value); svmtStr: PString(m_val)^ := String(value); end; end else begin if m_val <> nil then FreeMem(m_val); m_type := t; case t of svmtWord: begin New(PCardinal(m_val)); PCardinal(m_val)^ := Cardinal(value); end; svmtInt: begin New(PInt64(m_val)); PInt64(m_val)^ := Int64(value); end; svmtReal: begin New(PDouble(m_val)); PDouble(m_val)^ := Double(value); end; svmtStr: begin New(PString(m_val)); PString(m_val)^ := String(value); end; else Error(reVarTypeCast); end; end; end; ... procedure TSVMMem.SetW(value:cardinal); inline; begin if (m_val <> nil) and (m_type = svmtWord) then PCardinal(m_val)^ := value else begin if m_val <> nil then FreeMem(m_val); m_type := svmtWord; New(PCardinal(m_val)); PCardinal(m_val)^ := value; end; end; 

Jetzt können Sie Code für ein paar Getter schreiben:

 function TSVMMem.GetW:cardinal; inline; begin Result := 0; case m_type of svmtWord: Result := PCardinal(m_val)^; svmtInt: Result := PInt64(m_val)^; svmtReal: Result := Trunc(PDouble(m_val)^); svmtStr: Result := StrToQWord(PString(m_val)^); else Error(reVarTypeCast); end; end; 

Ok, großartig, jetzt, da Sie eine Weile auf die IDE gestarrt und den Code für Setter und Getter begeistert eingegeben haben, stehen wir vor der Aufgabe, Unterstützung für unsere Art von mathematischen und logischen Operationen zu implementieren. Als Beispiel werde ich die Implementierung der Additionsoperation geben:

 procedure TSVMMem.OpAdd(m:TSVMMem); inline; begin case m_type of svmtWord: case m.m_type of svmtWord: SetW(GetW + m.GetW); svmtInt: SetI(GetW + m.GetI); svmtReal: SetD(GetW + m.GetD); svmtStr: SetD(GetW + StrToFloat(m.GetS)); else Error(reVarInvalidOp); end; svmtInt: case m.m_type of svmtWord: SetI(GetI + m.GetW); svmtInt: SetI(GetI + m.GetI); svmtReal: SetD(GetI + m.GetD); svmtStr: SetD(GetI + StrToFloat(m.GetS)); else Error(reVarInvalidOp); end; svmtReal: case m.m_type of svmtWord: SetD(GetD + m.GetW); svmtInt: SetD(GetD + m.GetI); svmtReal: SetD(GetD + m.GetD); svmtStr: SetD(GetD + StrToFloat(m.GetS)); else Error(reVarInvalidOp); end; svmtStr: case m.m_type of svmtWord: SetS(GetS + IntToStr(m.GetW)); svmtInt: SetS(GetS + IntToStr(m.GetI)); svmtReal: SetS(GetS + FloatToStr(m.GetD)); svmtStr: SetS(GetS + m.GetS); else Error(reVarInvalidOp); end; else Error(reVarInvalidOp); end; end; 

Alles ist einfach. Weitere Operationen können auf ähnliche Weise beschrieben werden, und jetzt ist unsere Klasse bereit.
Für Arrays benötigen Sie natürlich noch einige Methoden, ein Beispiel für das Abrufen eines Elements per Index:

 function TSVMMem.ArrGet(index: cardinal; grabber:PGrabber): pointer; inline; begin Result := nil; case m_type of svmtArr: Result := PMemArray(m_val)^[index]; svmtStr: begin Result := TSVMMem.CreateFW(Ord(PString(m_val)^[index])); grabber^.AddTask(Result); end; else Error(reInvalidOp); end; end; 

Großartig. Jetzt können wir weitermachen.

Wir realisieren einen Stapel


Nach einer Weile kam ich zu solchen Gedanken. Der Stapel muss gleichzeitig statisch (aus Gründen der Geschwindigkeit) und dynamisch (aus Gründen der Flexibilität) sein.

Daher ist der Stapel in Blöcken implementiert. Das heißt, wie es funktionieren sollte - anfangs hat das Array des Stapels eine bestimmte Größe (ich habe beschlossen, die Blockgröße auf 256 Elemente festzulegen, damit es schön und nicht klein ist). Dementsprechend ist ein Array in dem Array enthalten, das die aktuelle Oberseite des Stapels anzeigt. Die Neuzuweisung von Speicher ist eine besonders lange Operation, die weniger häufig ausgeführt werden kann. Wenn mehr Werte auf den Stapel verschoben werden, kann seine Größe immer auf die Größe eines anderen Blocks erweitert werden.

Ich bringe die gesamte Stack-Implementierung mit:

 type TStack = object public items: array of pointer; size, i_pos: cardinal; parent_vm: pointer; procedure init(vm: pointer); procedure push(p: pointer); function peek: pointer; procedure pop; function popv: pointer; procedure swp; procedure drop; end; PStack = ^TStack; procedure TStack.init(vm: pointer); begin SetLength(items, StackBlockSize); i_pos := 0; size := StackBlockSize; parent_vm := vm; end; procedure TStack.push(p: pointer); inline; begin items[i_pos] := p; inc(i_pos); if i_pos >= size then begin size := size + StackBlockSize; SetLength(items, size) end; end; function TStack.peek: pointer; inline; begin Result := items[i_pos - 1]; end; procedure TStack.pop; inline; begin dec(i_pos); if size - i_pos > StackBlockSize then begin size := size - StackBlockSize; SetLength(items, size); end; end; function TStack.popv: pointer; inline; begin dec(i_pos); Result := items[i_pos]; if size - i_pos > StackBlockSize then begin size := size - StackBlockSize; SetLength(items, size); end; end; procedure TStack.swp; inline; var p: pointer; begin p := items[i_pos - 2]; items[i_pos - 2] := items[i_pos - 1]; items[i_pos - 1] := p; end; procedure TStack.drop; inline; begin SetLength(items, StackBlockSize); size := StackBlockSize; i_pos := 0; end; 

Bei externen Methoden übergibt die VM einen Zeiger auf den Stapel, damit sie die erforderlichen Argumente von dort übernehmen kann. Ein Zeiger auf den VM-Stream wurde später hinzugefügt, damit Rückrufaufrufe von externen Methoden implementiert werden können und im Allgemeinen mehr Leistung über VM-Methoden übertragen werden kann.

Also, wie Sie sich mit der Anordnung des Stapels vertraut gemacht haben. Der Rückrufstapel ist auf die gleiche Weise angeordnet, um die Call & Return-Operationen und den Garbage Collector-Stapel zu vereinfachen und zu vereinfachen. Das einzige, was ist die anderen Größen der Blöcke.

Sprechen Sie über Müll


Es ist normalerweise viel, viel. Und du musst etwas damit anfangen.

Zunächst möchte ich darüber sprechen, wie Garbage Collectors in anderen Sprachen angeordnet sind, z. B. in Lua, Ruby, Java, Perl, PHP usw. Sie arbeiten nach dem Prinzip, Zeiger auf Objekte im Speicher zu zählen.

Das heißt, Es ist also logisch, dass wir Speicher für etwas zugewiesen haben. Der Zeiger wurde sofort in einer Variablen / einem Array / an einer anderen Stelle platziert. Der Laufzeit-Garbage-Collector fügt diesen Zeiger sofort mit einer Liste möglicher Garbage-Objekte zu sich selbst hinzu. Im Hintergrund überwacht der Garbage Collector ständig alle Variablen, Arrays usw. Wenn es keinen Zeiger auf etwas aus der Liste des möglichen Mülls gibt, bedeutet dies, dass Müll und Speicher darunter entfernt werden müssen.

Ich habe beschlossen, mein Fahrrad zu verkaufen. Ich bin eher daran gewöhnt, mit dem Gedächtnis nach dem Prinzip von Taras Bulba zu arbeiten. Ich habe dich geboren - ich werde dich töten, ich meine, wenn ich den nächsten Freien in der nächsten Klasse rufe. Daher ist der Garbage Collector meiner VM halbautomatisch. Das heißt, Es muss im manuellen Modus aufgerufen werden und entsprechend damit arbeiten. In seinem Zug werden Zeiger auf deklarierte temporäre Objekte hinzugefügt (diese Rolle liegt hauptsächlich beim Übersetzer und ein wenig beim Entwickler). Um Speicher unter anderen Objekten freizugeben, können Sie einen separaten Opcode verwenden.

Das heißt, Der Garbage Collector verfügt zum Zeitpunkt des Aufrufs über eine vorgefertigte Liste von Zeigern, die Sie benötigen, um Speicherplatz freizugeben.

Nun beschäftigen wir uns mit der Kompilierung in eine abstrakte ausführbare Datei


Die Idee war ursprünglich, dass Anwendungen, die in meiner Sprache geschrieben wurden, ohne Quelle ausgeführt werden können, wie dies bei vielen ähnlichen Sprachen der Fall ist. Das heißt, es kann für kommerzielle Zwecke verwendet werden.

Bestimmen Sie dazu das Format der ausführbaren Dateien. Ich habe folgendes bekommen:

  1. Header, zum Beispiel "SVMEXE_CNS".
  2. Ein Abschnitt mit einer Liste von Bibliotheken, aus denen Methoden importiert werden.
  3. Der Importabschnitt der erforderlichen Methoden, die Bibliotheken, aus denen die Methoden importiert werden, werden durch ihre Nummer im obigen Abschnitt angegeben.
  4. Abschnitt der Konstanten.
  5. Codeabschnitt

Ich denke nicht, dass es sich lohnt, die detaillierten Schritte zum Implementieren von Parsern für diese Abschnitte darzulegen, da Sie alles in meinem Repository selbst sehen können.

Codeausführung


Nachdem wir die obigen Abschnitte analysiert und die VM initialisiert haben, haben wir einen Abschnitt mit dem Code. In meiner VM wird ein nicht ausgerichteter Bytecode ausgeführt, d. H. Anweisungen können beliebig lang sein.

Eine Reihe von Opcodes - Anweisungen für eine virtuelle Maschine mit kleinen Kommentaren, die ich im Voraus unten zeige:

 type TComand = ( {** for stack **} bcPH, // [top] = [var] bcPK, // [var] = [top] bcPP, // pop bcSDP, // stkdrop bcSWP, // [top] <-> [top-1] {** jump's **} bcJP, // jump [top] bcJZ, // [top] == 0 ? jp [top-1] bcJN, // [top] <> 0 ? jp [top-1] bcJC, // jp [top] & push callback point as ip+1 bcJR, // jp to last callback point & rem last callback point {** for untyped's **} bcEQ, // [top] == [top-1] ? [top] = 1 : [top] = 0 bcBG, // [top] > [top-1] ? [top] = 1 : [top] = 0 bcBE, // [top] >= [top-1] ? [top] = 1 : [top] = 0 bcNOT, // [top] = ![top] bcAND, // [top] = [top] and [top-1] bcOR, // [top] = [top] or [top-1] bcXOR, // [top] = [top] xor [top-1] bcSHR, // [top] = [top] shr [top-1] bcSHL, // [top] = [top] shl [top-1] bcNEG, // [top] = -[top] bcINC, // [top]++ bcDEC, // [top]-- bcADD, // [top] = [top] + [top-1] bcSUB, // [top] = [top] - [top-1] bcMUL, // [top] = [top] * [top-1] bcDIV, // [top] = [top] / [top-1] bcMOD, // [top] = [top] % [top-1] bcIDIV, // [top] = [top] \ [top-1] bcMV, // [top]^ = [top-1]^ bcMVBP, // [top]^^ = [top-1]^ bcGVBP, // [top]^ = [top-1]^^ bcMVP, // [top]^ = [top-1] {** memory operation's **} bcMS, // memory map size = [top] bcNW, // [top] = @new bcMC, // copy [top] bcMD, // double [top] bcRM, // rem @[top] bcNA, // [top] = @new array[ [top] ] of pointer bcTF, // [top] = typeof( [top] ) bcSF, // [top] = sizeof( [top] ) {** array's **} bcAL, // length( [top] as array ) bcSL, // setlength( [top] as array, {stack} ) bcPA, // push ([top] as array)[top-1] bcSA, // peek [top-2] -> ([top] as array)[top-1] {** memory grabber **} bcGPM, // add pointer to TMem to grabber task-list bcGC, // run grabber {** constant's **} bcPHC, // push copy of const bcPHCP, // push pointer to original const {** external call's **} bcPHEXMP, // push pointer to external method bcINV, // call external method bcINVBP, // call external method by pointer [top] {** for thread's **} bcPHN, // push null bcCTHR, // [top] = thread(method = [top], arg = [top+1]):id bcSTHR, // suspendthread(id = [top]) bcRTHR, // resumethread(id = [top]) bcTTHR, // terminatethread(id = [top]) {** for try..catch..finally block's **} bcTR, // try @block_catch = [top], @block_end = [top+1] bcTRS, // success exit from try/catch block bcTRR, // raise exception, message = [top] {** for string's **} bcSTRD, // strdel bcCHORD, bcORDCH, {** [!] directly memory operations **} bcALLC, //alloc memory bcRALLC, //realloc memory bcDISP, //dispose memory bcGTB, //get byte bcSTB, //set byte bcCBP, //mem copy bcRWBP, //read word bcWWBP, //write word bcRIBP, //read int bcWIBP, //write int bcRFBP, //read float bcWFBP, //write float bcRSBP, //read string bcWSBP, //write string bcTHREXT,//stop code execution bcDBP //debug method call ); 

Sie haben sich also fließend mit den von mir geschriebenen Vorgängen vertraut gemacht. Jetzt möchte ich darüber sprechen, wie alles funktioniert.

Eine VM wird als Objekt implementiert, sodass Sie problemlos Multithreading-Unterstützung implementieren können.

Es hat einen Zeiger auf ein Array mit Opcodes, IP (Instruction Pointer) - Offset des ausgeführten Befehls und Zeiger auf andere VM-Strukturen.

Die Codeausführung ist ein großer Switch-Fall.

Geben Sie einfach eine Beschreibung der VM:

 type TSVM = object public ip, end_ip: TInstructionPointer; mainclasspath: string; mem: PMemory; stack: TStack; cbstack: TCallBackStack; bytes: PByteArr; grabber: TGrabber; consts: PConstSection; extern_methods: PImportSection; try_blocks: TTRBlocks; procedure Run; procedure RunThread; procedure LoadByteCodeFromFile(fn: string); procedure LoadByteCodeFromArray(b: TByteArr); end; 

Ein bisschen über die Ausnahmebehandlung


Zu diesem Zweck verfügt die VM über einen Stapel von Ausnahmebehandlungsroutinen und einen großen Try / Catch-Block, in den die Codeausführung eingeschlossen ist. Aus dem Stapel können Sie eine Struktur mit einem Eintrittspunktversatz auf den Block für die Behandlung von Catch- und End- / End-Ausnahmen setzen. Ich habe auch den trs-Opcode bereitgestellt, der vor catch platziert wird und den Code auf finally / end setzt, wenn dies erfolgreich ist, und gleichzeitig den Block mit Informationen zu Ausnahmebehandlungsroutinen vom oberen Rand des entsprechenden Stapels gelöscht. Ist es einfach Einfach. Ist es bequem? Praktisch.

Lassen Sie uns über externe Methoden und Bibliotheken sprechen


Ich habe sie bereits erwähnt. Importe, Bibliotheken ... Ohne sie hat die Sprache nicht die gewünschte Flexibilität und Funktionalität.

Zunächst deklarieren wir bei der Implementierung der VM den Typ der externen Methode und das Protokoll für den Aufruf.

 type TExternalFunction = procedure(PStack: pointer); cdecl; PExternalFunction = ^TExternalFunction; 

Beim Importieren einer VM füllt der Parser des Importabschnitts ein Array von Zeigern auf externe Methoden. Daher hat jede Methode eine statische Adresse, die in der Phase der Montage der Anwendung unter der VM berechnet wird und über die die gewünschte Methode aufgerufen werden kann.

Der Aufruf erfolgt später auf folgende Weise während der Codeausführung:

 TExternalFunction(self.extern_methods^.GetFunc(TSVMMem(self.stack.popv).GetW))(@self.stack); 

Schreiben wir eine einfache Bibliothek für unsere VM


Und lassen Sie sie zuerst die Schlafmethode implementieren:

 library bf; {$mode objfpc}{$H+} uses SysUtils, svm_api in '..\svm_api.pas'; procedure DSleep(Stack:PStack); cdecl; begin sleep(TSVMMem(Stack^.popv).GetW); end; exports DSleep name 'SLEEP'; end. 

Zusammenfassung


Dazu werde ich wahrscheinlich meinen ersten Artikel aus einem geplanten Zyklus beenden.

Heute habe ich die Erstellung der Sprachlaufzeit ausführlich beschrieben. Ich glaube, dass dieser Artikel sehr nützlich für Leute sein wird, die sich entscheiden, ihre eigene Sprache zu schreiben oder zu verstehen, wie ähnliche Programmiersprachen funktionieren.

Der vollständige VM-Code ist im Repository im Zweig / runtime / svm verfügbar.

Wenn Ihnen dieser Artikel gefallen hat, dann seien Sie nicht faul, ein Plus an Karma zu werfen und es nach oben zu heben. Ich habe es versucht und werde es für Sie versuchen.

Wenn Ihnen etwas nicht klar ist, dann begrüßen Sie die Kommentare oder das Forum .

Vielleicht sind Ihre Fragen und Antworten nicht nur für Sie interessant.

Source: https://habr.com/ru/post/de435202/


All Articles