
Und wieder habe ich das Volumen des Artikels überschätzt! Ich plante, dass dies der letzte Artikel sein würde, in dem wir einen Compiler erstellen und Tests durchführen werden. Aber das Volumen stellte sich als groß heraus und ich beschloss, den Artikel in zwei Teile zu teilen.
In diesem Artikel werden wir fast alle Grundfunktionen des Compilers ausführen. Es wird zum Leben erweckt und es wird möglich sein, ziemlich seriösen Code zu schreiben, zu kompilieren und auszuführen. Und wir werden im nächsten Teil Tests durchführen. (Übrigens die vorherigen Teile:
eins ,
zwei ,
drei ).
Ich schreibe zum ersten Mal in Habré, vielleicht ist es nicht immer in Ordnung. Meiner Meinung nach erwiesen sich die Artikel 2, 3 als ziemlich trocken, viel Code, wenig Beschreibung. Dieses Mal werde ich versuchen, etwas anderes zu machen und mich auf die Beschreibung der Ideen selbst konzentrieren. Nun, der Code ... der Code, natürlich wird es! Wer es gründlich verstehen will, wird eine solche Gelegenheit haben. In vielen Fällen werde ich den Code unter den Spoiler stellen. Und natürlich können Sie immer die vollständige Quelle auf dem Github anzeigen.
Der Compiler schreibt noch einige Zeit in Assembler, geht dann aber zum Fort und schreibt den Compiler weiter auf uns. Dies wird Baron Münchhausen ähneln, der sich an den Haaren aus dem Sumpf zog. Aber für den Anfang werde ich skizzieren, wie der Compiler auf dem Fort funktioniert. Willkommen bei Katze!
Wie funktioniert der Compiler?
Der Speicher in der Festung besteht aus einem fortlaufenden Fragment, in dem Wörterbucheinträge nacheinander angeordnet sind. Nach ihrer Fertigstellung folgt ein freier Speicherbereich. Das erste freie Byte wird durch die Variable h angezeigt. Es gibt hier auch das häufig verwendete Wort, das die Adresse des ersten freien Bytes auf dem Stapel verschiebt. Es wird sehr einfach bestimmt:
: here h @ ;

Erwähnenswert ist das Wort allot, das die angegebene Anzahl von Bytes durch Bewegen des Zeigers h reserviert. Das Wort Zuteilung kann wie folgt definiert werden:
: allot h +! ;
Tatsächlich verwendet der Compiler einen speziellen Interpreter-Modus sowie einige spezielle Wörter. Mit einem Satz können Sie also das gesamte Prinzip des Compilers in der Festung beschreiben. In welchem Modus der Interpreter arbeitet, wird durch die Zustandsvariable bestimmt. Wenn es Null ist, wird der Ausführungsmodus eingestellt, andernfalls - Kompilierungsmodus. Wir kennen den Ausführungsmodus bereits, darin werden die Wörter aus dem Eingabepuffer einfach nacheinander ausgeführt. Im Kompilierungsmodus werden sie jedoch nicht ausgeführt, sondern vom Zeiger h in den Speicher kompiliert. Dementsprechend bewegt sich der Zeiger vorwärts.
In der klassischen Festung wird das Wort "," verwendet, um einen ganzzahligen Wert zu kompilieren, das Wort "c" wird verwendet, um ein Byte zu kompilieren. Unser System verwendet Werte unterschiedlicher Bittiefe (8, 16, 32, 64), daher werden zusätzlich die Wörter "w" und "i" verwendet. Wir machen auch das Wort "str", das die Zeichenfolge kompiliert und zwei Werte aus dem Stapel entnimmt - die Adresse und die Länge der Zeichenfolge.
Spezielle Compilerwörter werden verwendet, um Kontrollstrukturen zu bilden. Dies sind die Wörter if, do, loop und andere. Diese Wörter werden auch im Kompilierungsmodus ausgeführt. Beispiel: Das Wort if kompiliert bei der Ausführung einen bedingten Verzweigungsbytebefehl (? Nbranch). Damit das System weiß, welche Wörter im Kompilierungsmodus ausgeführt und nicht kompiliert werden müssen, wird das unmittelbare Flag (Vorzeichen) verwendet. Wir haben es bereits im Flag-Feld des Wörterbucheintrags. Im Assembler-Quellcode heißt es f_immediate. Verwenden Sie das Wort sofort, um dieses Flag zu setzen. Es hat keine Parameter, das unmittelbare Flag wird beim letzten Wort im Wörterbuch gesetzt.
Gehen wir jetzt von der Theorie zur Praxis!
Vorbereitung
Am Anfang müssen wir einige einfache Bytebefehle in der Assemblersprache ausführen, die wir benötigen. Hier sind sie: Verschieben (Kopieren des Speicherbereichs), Füllen (Füllen des Speicherbereichs), Bitoperationen (und / oder xor, Invertieren), Bitverschiebungsbefehle (rshift, lshift). Lassen Sie uns den gleichen rpick machen (dies ist das gleiche wie pick, es funktioniert nur mit dem Rückgabestapel, nicht mit dem Datenstapel).
Diese Befehle sind sehr einfach, hier ist ihr Code b_move = 0x66 bcmd_move: pop rcx pop rdi pop rsi repz movsb jmp _next b_fill = 0x67 bcmd_fill: pop rax pop rcx pop rdi repz stosb jmp _next b_rpick = 0x63 bcmd_rpick: pop rcx push [rbp + rcx * 8] jmp _next b_and = 0x58 bcmd_and: pop rax and [rsp], rax jmp _next b_or = 0x59 bcmd_or: pop rax or [rsp], rax jmp _next b_xor = 0x5A bcmd_xor: pop rax xor [rsp], rax jmp _next b_invert = 0x5B bcmd_invert: notq [rsp] jmp _next b_rshift = 0x5C bcmd_rshift: pop rcx or rcx, rcx jz _next 1: shrq [rsp] dec rcx jnz 1b jmp _next b_lshift = 0x5D bcmd_lshift: pop rcx or rcx, rcx jz _next 1: shlq [rsp] dec rcx jnz 1b jmp _next
Müssen noch das Wort Wort machen. Dies ist dasselbe wie blword, jedoch wird auf dem Stapel ein bestimmtes Trennzeichen angegeben. Ich gebe den Code nicht an, er befindet sich in der Quelle. Ich habe die Wörter blworld kopiert / eingefügt und die Vergleichsbefehle ersetzt.
Abschließend machen wir das Wort syscall. Damit ist es möglich, die fehlenden Systemoperationen durchzuführen, beispielsweise mit Dateien zu arbeiten. Eine solche Lösung funktioniert nicht, wenn Plattformunabhängigkeit erforderlich ist. Aber dieses System wird jetzt für Tests verwendet, also lass es vorerst so sein. Bei Bedarf können alle Operationen in Bytebefehle konvertiert werden, es ist überhaupt nicht schwierig. Der Befehl syscall akzeptiert 6 Parameter für den Systemaufruf und die Rufnummer vom Stapel. Es wird ein Parameter zurückgegeben. Die Parameterzuweisungen und der Rückgabewert werden durch die Systemrufnummer bestimmt.
b_syscall = 0xFF bcmd_syscall: sub rbp, 8 mov [rbp], r8 pop rax pop r9 pop r8 pop r10 pop rdx pop rsi pop rdi syscall push rax mov r8, [rbp] add rbp, 8 jmp _next
Und jetzt gehen wir direkt zum Compiler.
Compiler
Erstellen wir die Variable h, hier ist alles einfach.
item h h: .byte b_var0 .quad 0
Wir werden die Initialisierung in die Startzeile schreiben:
# forth last_item context @ ! h dup 8 + swap ! quit start: .byte b_call16 .word forth - . - 2 .byte b_call16 .word last_item - . - 2 .byte b_call16 .word context - . - 2 .byte b_get .byte b_set .byte b_call16 .word h - . - 2 .byte b_dup, b_num8, b_add, b_swap, b_set .byte b_quit
Lassen Sie uns hier das Wort machen:
item here .byte b_call8, h - . - 1 .byte b_get .byte b_exit
Und auch Wörter zum Kompilieren der Werte: "allot" und "c", "w", "i", ",", "str" # : allot h +! ; item allot allot: .byte b_call8, h - . - 1, b_setp, b_exit # : , here ! 8 allot ; item "," .byte b_call8, here - . - 1, b_set, b_num8, b_call8, allot - . - 1, b_exit # : i, here i! 4 allot ; item "i," .byte b_call8, here - . - 1, b_set32, b_num4, b_call8, allot - . - 1, b_exit # : w, here w! 2 allot ; item "w," .byte b_call8, here - . - 1, b_set16, b_num2, b_call8, allot - . - 1, b_exit # : c, here c! 1 allot ; item "c," .byte b_call8, here - . - 1, b_set8, b_num1, b_call8, allot - . - 1, b_exit # : str, dup -rot dup c, here swap move 1+ h +!; item "str," c_str: .byte b_dup, b_mrot, b_dup callb c_8 callb here .byte b_swap, b_move callb h .byte b_setp .byte b_exit
Lassen Sie uns nun die Statusvariable und zwei Wörter erstellen, um ihren Wert zu steuern: "[" und "]". Normalerweise werden diese Wörter verwendet, um zum Zeitpunkt der Kompilierung etwas auszuführen. Daher deaktiviert das Wort "[" den Kompilierungsmodus und das Wort "]" ihn. Nichts hindert sie jedoch daran, in anderen Fällen verwendet zu werden, wenn der Kompilierungsmodus ein- oder ausgeschaltet werden muss. Das Wort "[" wird unser erstes Wort mit dem unmittelbaren Zeichen sein. Andernfalls kann der Kompilierungsmodus nicht deaktiviert werden, da er kompiliert und nicht ausgeführt wird.
item state .byte b_var0 .quad 0 item "]" .byte b_num1 callb state .byte b_set, b_exit item "[", f_immediate .byte b_num0 callb state .byte b_set, b_exit
Die Wende kam für das Wort $ compile. Es wird die Adresse des Wörterbucheintrags vom Stapel nehmen und das angegebene Wort kompilieren. Um ein Wort in gewöhnlichen Fort-Implementierungen zu kompilieren, reicht es aus, das Wort "," auf die Ausführungsadresse anzuwenden. Hier ist alles viel komplizierter. Erstens gibt es zwei Arten von Wörtern - Bytecode und Maschinencode. Die ersteren werden nach Byte kompiliert, die letzteren nach dem Befehl call byte. Und zweitens - wir haben bis zu vier Varianten des Aufrufbefehls: call8, call16, call32 und call64. Vier? Nein! Als ich den Compiler schrieb, fügte ich diesen vier 16 weitere hinzu! :) :)
Wie ist das passiert? Wir müssen einen kleinen Exkurs machen.
Anrufbefehl verbessern
Als der Compiler anfing zu arbeiten, stellte ich fest, dass in vielen Fällen (aber nicht in allen Fällen) der Befehl call8 ausreicht. Dies ist der Fall, wenn das aufgerufene Wort innerhalb von 128 Bytes liegt. Ich dachte - und wie kann ich sicherstellen, dass dies in fast allen Fällen passiert? Wie füge ich mehr als 256 Werte in ein Byte ein?
Der erste Punkt, den ich bemerkte, war, dass in der Festung der Anruf immer in Richtung niedrigerer Adressen geht. Dies bedeutet, dass Sie den Aufrufbefehl so wiederholen können, dass nur niedrigere Adressen aufgerufen werden können, jedoch für 256 Byte, nicht für 128. Es ist besser.
Aber wenn Sie irgendwo ein paar Kleinigkeiten platzieren ... Es stellt sich heraus, dass dort wo ist! Wir haben zwei Bytes: Ein Byte ist der Befehl, das zweite ist der Offset. Nichts hindert die unteren Bits des Befehls daran, die oberen Bits des Parameters (Offset) zu platzieren. Bei einer Byte-Maschine sieht es so aus, als gäbe es anstelle eines Aufrufbefehls mehrere. Ja, auf diese Weise belegen wir mehrere Zellen der Byte-Befehlscodetabelle mit einem Befehl, aber manchmal lohnt es sich, dies zu tun. Der Aufrufbefehl ist einer der am häufigsten verwendeten Befehle, daher habe ich beschlossen, 4 Versatzbits in den Befehl einzufügen. So können Sie in einer Entfernung von bis zu 4095 Bytes telefonieren! Dies bedeutet, dass ein solcher Kurzaufrufbefehl fast immer verwendet wird. Ich habe diese Befehle mit dem Code 0xA0 platziert und die folgenden Zeilen wurden in der Befehlstabelle angezeigt:
.quad bcmd_call8b0, bcmd_call8b1, bcmd_call8b2, bcmd_call8b3, bcmd_call8b4, bcmd_call8b5, bcmd_call8b6, bcmd_call8b7 # 0xA0 .quad bcmd_call8b8, bcmd_call8b9, bcmd_call8b10, bcmd_call8b11, bcmd_call8b12, bcmd_call8b13, bcmd_call8b14, bcmd_call8b15
Der erste dieser Bytebefehle ruft einfach in Richtung niedrigerer Adressen mit dem im Parameter angegebenen Versatz (bis zu 255) auf. Der Rest fügt dem Parameter den entsprechenden Offset hinzu. bcmd_call8b1 fügt 256 hinzu, bcmd_call8b2 fügt 512 hinzu und so weiter. Ich habe den ersten Aufrufbefehl separat ausgeführt, der Rest mit einem Makro.
Erster Befehl:
b_call8b0 = 0xA0 bcmd_call8b0: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 sub r8, rax jmp _next
Makro und Erstellen der restlichen Aufrufbefehle:
.macro call8b N b_call8b\N = 0xA\N bcmd_call8b\N: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 add rax, \N * 256 mov [rbp], r8 sub r8, rax jmp _next .endm call8b 1 call8b 2 call8b 3 call8b 4 call8b 5 call8b 6 call8b 7 call8b 8 call8b 9 call8b 10 call8b 11 call8b 12 call8b 13 call8b 14 call8b 15
Nun, ich habe den alten Befehl call8 überarbeitet, um weiterzuleiten, da wir bereits 16 Teams haben, die einen Rückruf tätigen. Was auch immer die Verwirrung war, ich habe es in b_call8f umbenannt:
b_call8f = 0x0C bcmd_call8f: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 add r8, rax jmp _next
Übrigens habe ich der Einfachheit halber ein Makro erstellt, das in Assembler den entsprechenden Rückruf innerhalb von 4095 automatisch kompiliert. Und dann musste ich nie :)
.macro callb adr .if \adr > . .error "callb do not for forward!" .endif .byte b_call8b0 + (. - \adr + 1) >> 8 .byte (. - \adr + 1) & 255 .endm
Und jetzt…
Teamzusammenstellung
Wir erhalten also einen ziemlich komplizierten Befehlskompilierungsalgorithmus. Wenn dies ein Bytebefehl ist, kompilieren Sie nur ein Byte (Bytebefehlscode). Und wenn dieses Wort bereits in Bytecode geschrieben ist, müssen Sie seinen Aufruf mit dem Befehl call kompilieren und einen von zwanzig auswählen. Genauer gesagt 19, wir haben also keine Anrufweiterleitung und call8f wird nicht für das Fort verwendet.
Die Wahl ist also diese. Wenn der Versatz innerhalb von 0 ...- 4095 liegt, wählen Sie den Befehl bcmd_call8b mit dem Code 0xA0 aus und platzieren Sie die vier höchstwertigen Versatzbits in den niedrigstwertigen Bits des Befehls. Gleichzeitig lautet der Code für einen der Befehle bcmd_call8b0 für die Byte-Maschine bcmd_call8b15.
Wenn der Rückwärtsversatz größer oder gleich 4095 ist, bestimmen wir, in welcher Dimension der Versatz platziert wird, und verwenden den entsprechenden Befehl von call16 / 32/64. Es ist zu beachten, dass der Offset für diese Teams unterschrieben ist. Sie können sowohl vorwärts als auch rückwärts verursachen. Beispielsweise kann call16 eine Entfernung von 32767 in beide Richtungen anrufen.
Hier ist die Implementierung als Ergebnis:
$ kompilierenKompiliert ein Wort. Nimmt als Parameter die Adresse des Wörterbucheintrags des kompilierten Wortes. Tatsächlich überprüft es das Flag f_code, berechnet die Code-Adresse (cfa) und ruft compile_b oder compile_c auf (wenn das Flag gesetzt ist).
compile_cKompiliert einen Bytebefehl. Das einfachste Wort hier wird auf der Festung folgendermaßen beschrieben:
: compile_c c@ c, ;
compile_bEs nimmt eine Bytecode-Adresse auf dem Stapel und kompiliert seinen Aufruf.
test_bvEs nimmt einen Offset vom Stapel (mit einem Vorzeichen) und bestimmt, welche Bittiefe verwendet werden soll (1, 2, 4 oder 8 Bytes). Gibt den Wert 0, 1, 2 oder 3 zurück. Mit diesem Wort können Sie anhand der Befehle call16 / 32/64 bestimmen, welches verwendet werden soll. Dieses Wort ist nützlich beim Zusammenstellen von Zahlen (eine Auswahl aus lit8 / 16/32/64).
Übrigens können Sie das System starten und in der Fort-Konsole mit einem dieser Wörter „herumspielen“. Zum Beispiel:
$ ./forth ( 0 ): > 222 test_bv ( 2 ): 222 1 > drop drop ( 0 ): > 1000000 test_bv ( 2 ): 1000000 2 > drop drop ( 0 ): > -33 test_bv ( 2 ): -33 0 >
test_bvcEs nimmt einen Offset (mit einem Vorzeichen) vom Stapel und bestimmt, welcher Aufrufbefehl verwendet werden soll. Tatsächlich prüft es, ob der Offset im Bereich von 0 ... -4095 liegt, und gibt 0 zurück. In diesem Fall ruft es test_bv auf, wenn in diesem Intervall kein Treffer erfolgt.
Das ist alles, was Sie zum Kompilieren des Befehls benötigen. # : test_bvc dup 0 >= over FFF <= and if 0 exit else ... item test_bvc test_bvc: .byte b_dup, b_neg .byte b_num0 .byte b_gteq .byte b_over, b_neg .byte b_lit16 .word 0xFFF .byte b_lteq .byte b_and .byte b_qnbranch8, 1f - . .byte b_num0 .byte b_exit item test_bv test_bv: .byte b_dup, b_lit8, 0x80, b_gteq, b_over, b_lit8, 0x7f, b_lteq, b_and, b_qnbranch8, 1f - ., b_num0 .byte b_exit 1: .byte b_dup .byte b_lit16 .word 0x8001 .byte b_gteq .byte b_over .byte b_lit16 .word 0x7ffe .byte b_lteq, b_and, b_qnbranch8, 2f - ., b_num1, b_exit 2: .byte b_dup .byte b_lit32 .int 0x80000002 .byte b_gteq .byte b_over .byte b_lit32 .int 0x7ffffffd .byte b_lteq, b_and, b_qnbranch8, 3f - ., b_num2, b_exit 3: .byte b_num3 .byte b_exit # - item compile_c compile_c: .byte b_get8 callb c_8 .byte b_exit # - item compile_b compile_b: callb here .byte b_num2, b_add .byte b_sub callb test_bvc .byte b_dup .byte b_zeq .byte b_qnbranch8, 1f - . .byte b_drop .byte b_neg .byte b_dup .byte b_lit8, 8 .byte b_rshift .byte b_lit8, b_call8b0 .byte b_or callb c_8 callb c_8 .byte b_exit 1: .byte b_dup, b_num1, b_eq, b_qnbranch8, 2f - ., b_drop, b_lit8, b_call16 callb c_8 .byte b_wm callb c_16 .byte b_exit 2: .byte b_num2, b_eq, b_qnbranch8, 3f - ., b_lit8, b_call32 callb c_8 .byte b_num3, b_sub callb c_32 .byte b_exit 3: .byte b_lit8, b_call64 callb c_8 .byte b_lit8, 7, b_sub callb c_64 .byte b_exit #: $compile dup c@ 0x80 and if cfa compile_c else cfa compile_b then ; item "$compile" _compile: .byte b_dup, b_get8, b_lit8, 0x80, b_and, b_qnbranch8, 1f - ., b_cfa callb compile_c .byte b_exit 1: .byte b_cfa callb compile_b .byte b_exit
Jetzt müssen wir die Nummer zusammenstellen.
Zusammenstellung einer Zahl (Literal)
Schrieb einen ganzen Untertitel, der darauf vorbereitet war, die Zusammenstellung des Literal spezifisch zu beschreiben, aber es stellt sich heraus, dass es nichts Besonderes zu beschreiben gibt :)
Wir haben bereits die Hälfte der Arbeit im Wort test_bv erledigt. Es bleibt nur noch test_bv aufzurufen und je nach Ergebnis lit8 / 16/32/64 und dann den entsprechenden Wert von 1, 2, 4 oder 8 Bytes zu kompilieren.
Dazu definieren wir das Wort compile_n # item compile_n compile_n: callb test_bv .byte b_dup .byte b_zeq .byte b_qnbranch8, 1f - . .byte b_drop, b_lit8, b_lit8 callb c_8 callb c_8 .byte b_exit 1: .byte b_dup, b_num1, b_eq, b_qnbranch8, 2f - ., b_drop, b_lit8, b_lit16 callb c_8 callb c_16 .byte b_exit 2: .byte b_num2, b_eq, b_qnbranch8, 3f - ., b_lit8, b_lit32 callb c_8 callb c_32 .byte b_exit 3: .byte b_lit8, b_lit64 callb c_8 callb c_64 .byte b_exit
Ändern Sie den Interpreter
Alles ist bereit, den Befehl und die Literale zu kompilieren. Jetzt muss es in den Interpreter eingebaut werden. Diese Änderung ist einfach. Fügen Sie an der Stelle, an der der Befehl ausgeführt wurde, die Statusprüfung hinzu. Wenn state nicht null ist und das Wort nicht das unmittelbare Flag enthält, müssen Sie anstelle der Ausführung $ compile aufrufen. Und ungefähr das Gleiche, wenn die Nummer aus dem Eingabestream abgerufen wird. Wenn state null ist, lassen Sie einfach die Nummer auf dem Stapel und rufen Sie compile_n auf, wenn nicht.
Hier ist der Dolmetscher item interpret interpret: .byte b_blword .byte b_dup .byte b_qnbranch8 .byte 0f - . .byte b_over .byte b_over .byte b_find .byte b_dup .byte b_qnbranch8 .byte 1f - . .byte b_mrot .byte b_drop .byte b_drop callb state .byte b_get .byte b_qnbranch8, irpt_execute - . # 0, .byte b_dup, b_get8, b_lit8, f_immediate, b_and # immediate .byte b_qbranch8, irpt_execute - . # - # ! callb _compile .byte b_branch8, 2f - . irpt_execute: .byte b_cfa # , (state = 0 immediate ) .byte b_execute .byte b_branch8, 2f - . 1: .byte b_drop .byte b_over, b_over .byte b_numberq # , .byte b_qbranch8, 3f - . # 0, , 3 .byte b_type # .byte b_strp # .byte 19 # .ascii " : word not found!\n" .byte b_quit # 3: .byte b_nip, b_nip # , ( b_over, b_over) # - callb state # , .byte b_get .byte b_qnbranch8, 2f - . # - ; - # callb compile_n 2: # .byte b_depth # .byte b_zlt # , 0 ( 0<) .byte b_qnbranch8, interpret_ok - . # , , .byte b_strp # .byte 14 .ascii "\nstack fault!\n" .byte b_quit # interpret_ok: .byte b_branch8 .byte interpret - . 0: .byte b_drop .byte b_exit
Jetzt sind wir einen Schritt vom Compiler entfernt ...
Definition neuer Wörter (Wort ":")
Wenn wir nun die Statusvariable auf einen Wert ungleich Null setzen, beginnt der Kompilierungsprozess. Aber das Ergebnis wird nutzlos sein, wir können es weder erfüllen noch im Gedächtnis finden. Um dies alles zu ermöglichen, muss das Kompilierungsergebnis in Form eines Wörterbuchartikels formatiert werden. Dazu müssen Sie vor dem Aktivieren des Kompilierungsmodus einen Titel für das Wort erstellen.
Der Header sollte Flags, ein Kommunikationsfeld und einen Namen enthalten. Hier haben wir eine vertraute Geschichte - das Kommunikationsfeld kann 1, 2, 4 oder 8 Bytes sein. Machen wir das Wort compile_1248, das uns hilft, ein solches Kommunikationsfeld zu bilden. Der Stapel benötigt zwei Zahlen - den Offset und den vom Befehl test_bv generierten Wert.
compile_1248 # , , # , test_dv item compile_1248 compile_1248: .byte b_dup .byte b_zeq .byte b_qnbranch8, 1f - . .byte b_drop callb c_8 .byte b_exit 1: .byte b_dup, b_num1, b_eq, b_qnbranch8, 2f - . .byte b_drop callb c_16 .byte b_exit 2: .byte b_num2, b_eq, b_qnbranch8, 3f - . callb c_32 .byte b_exit 3: callb c_64 .byte b_exit
Machen Sie jetzt das Wort $ create. Es wird uns mehr als einmal nützlich sein. Sie können es verwenden, wenn Sie einen Titel für einen Wörterbucheintrag erstellen müssen. Es werden zwei Werte aus dem Stapel übernommen - die Adresse des Namens des erstellten Wortes und seine Länge. Nach dem Ausführen dieses Wortes wird die Adresse des erstellten Wörterbucheintrags auf dem Stapel angezeigt.
$ create # : $create here current @ @ here - test_bv dup c, compile_1248 -rot str, current @ ! ' var0 here c!; item "$create" create: callb here callb current .byte b_get, b_get callb here .byte b_sub callb test_bv .byte b_dup callb c_8 callb compile_1248 .byte b_mrot callb c_str # callb current .byte b_get, b_set # - var0, here # , - , # , # 1 allot , .byte b_lit8, b_var0 callb here .byte b_set8 .byte b_exit
Das nächste Wort nimmt den Namen des neuen Wortes mit dem Wort blword aus dem Eingabestream auf und ruft $ create auf, wodurch ein neues Wort mit dem angegebenen Namen erstellt wird.
create_in item "create_in" create_in: .byte b_blword .byte b_dup .byte b_qbranch8 .byte 1f - . .byte b_strp # ( ) .byte 3f - 2f # 2: .ascii "\ncreate_in - name not found!\n" 3: .byte b_quit 1: callb create .byte b_exit
Und schließlich machen Sie das Wort ":". Mit create_in wird ein neues Wort erstellt und der Kompilierungsmodus festgelegt. Es wird nicht installiert. Und wenn installiert, gibt es einen Fehler. Das Wort ":" hat das unmittelbare Zeichen.
Wort : # : : create_in 1 state dup @ if ." : - no execute state!" then ! 110 ; immediate item ":", f_immediate colon: callb create_in .byte b_num1 callb state .byte b_dup .byte b_get .byte b_qnbranch8, 2f - . .byte b_strp # ( ) .byte 4f - 3f # 3: .ascii "\n: - no execute state!\n" 4: .byte b_quit 2: .byte b_set .byte b_lit8, 110 .byte b_exit
Wenn jemand in den Code schaute, dann sah er, dass dieses Wort etwas anderes tut :)
Und hier ist 110 ???
Ja, dieses Wort drückt auch die Nummer 110 auf den Stapel, und deshalb. Beim Kompilieren müssen die verschiedenen Konstrukte ein einziges Ganzes sein. Zum Beispiel nach wenn muss dann sein. Und das mit ":" erstellte Wort sollte mit ";" enden. Um diese Bedingungen zu überprüfen, legen spezielle Wörter des Compilers bestimmte Werte auf den Stapel und prüfen, ob sie vorhanden sind. Zum Beispiel setzt das Wort ":" den Wert 110 und das Wort ";" prüft, ob 110 oben auf dem Stapel liegt. Wenn dies nicht der Fall ist, ist dies ein Fehler. Die Kontrollstrukturen wurden also nicht gepaart.
Eine solche Überprüfung wird in allen solchen Wörtern des Compilers durchgeführt, daher werden wir ein spezielles Wort dafür machen - "? Pairs". Es werden zwei Werte vom Stapel genommen und ein Fehler ausgegeben, wenn sie nicht gleich sind.
In solchen Worten müssen Sie häufig überprüfen, ob der Kompilierungsmodus eingestellt ist. Lassen Sie uns das Wort "? State" dafür machen.
"Paare" Zustand #: ?pairs = ifnot exit then ." \nerror: no pairs operators" quit then ; item "?pairs" .byte b_eq, b_qbranch8, 1f - . .byte b_strp .byte 3f - 2f 2: .ascii "\nerror: no pairs operators" 3: .byte b_quit 1: .byte b_exit #: ?state state @ 0= if abort" error: no compile state" then ; item "?state" callb state .byte b_get, b_zeq, b_qnbranch8, 1f - . .byte b_strp .byte 3f - 2f 2: .ascii "\nerror: no compile state" 3: .byte b_quit 1: .byte b_exit
Das ist alles! Wir werden nichts anderes in Assembler manuell kompilieren :)
Aber bis zum Ende ist der Compiler noch nicht geschrieben worden, so dass Sie am Anfang einige ungewöhnliche Methoden anwenden müssen ...
Machen wir uns bereit, den erstellten Compiler mit dem erstellten Compiler zu kompilieren
Zu Beginn können Sie überprüfen, wie das Wort ":" funktioniert, indem Sie etwas Einfaches kompilieren. Lassen Sie uns zum Beispiel das Wort machen:
: ^2 dup * ;
Dieses Wort ist quadratisch. Aber wir haben nicht das Wort ";" was zu tun?
Wir schreiben stattdessen das Wort exit und es wird kompiliert. Schalten Sie dann den Kompilierungsmodus mit dem Wort "[" aus und lassen Sie den Wert 110 fallen: $ ./forth ( 0 ): > : ^2 dup * exit [ drop ( 0 ): > 4 ^2 ( 1 ): 16 >
Es funktioniert! Fahren wirfort ...Da wir das Fort weiterhin auf dem Fort schreiben werden, müssen wir uns überlegen, wo sich der Quellcode des Forts befindet und wann kompiliert werden soll. Lassen Sie uns die einfachste Option machen. Der Quellcode der Festung wird im Assembler als Textzeichenfolge in den Quellcode des Assemblers eingefügt. Und damit er nicht zu viel Platz einnimmt, platzieren wir ihn unmittelbar nach der Adresse hier im freien Speicherbereich. Natürlich benötigen wir diesen Bereich für die Kompilierung, aber die Geschwindigkeit des "Durchgehens" der Interpretation ist größer als der Bedarf an neuem Speicher. Daher beginnt der kompilierte Code von Anfang an, die Quelle auf dem Fort zu überschreiben, aber wir werden ihn nicht mehr benötigen, da wir diesen Abschnitt bereits gelesen und verwendet haben. fcode: .ascii " 2 2 + . quit"
Am Anfang der Zeile lohnt es sich jedoch, ein Dutzend Leerzeichen zu platzieren.Damit dies funktioniert, ändern wir den Startbytecode so, dass tib, #tib auf diese Zeile zeigen. Am Ende wird beendet, um die normale Befehlszeile des Systems aufzurufen.Das Starten von Bytecode ist so geworden start: .byte b_call16 .word forth - . - 2 .byte b_call16 .word last_item - . - 2 .byte b_call16 .word context - . - 2 .byte b_get .byte b_set .byte b_call16 .word vhere - . - 2 .byte b_dup .byte b_call16 .word h - . - 2 .byte b_set .byte b_call16 .word definitions - . - 2 .byte b_call16 .word tib - . - 2 .byte b_set .byte b_lit16 .word fcode_end - fcode .byte b_call16 .word ntib - . - 2 .byte b_set .byte b_call16 .word interpret - . - 2 .byte b_quit
Starten Sie! $ ./forth 4 ( 0 ): >
Großartig!
Und jetzt…Kompilieren Sie den Compiler mit dem Compiler
Als nächstes schreiben wir den Code in die fcode-Zeile. Das erste, was zu tun ist, ist natürlich das Wort ";". : ; ?state 110 ?pairs lit8 [ blword exit find cfa c@ c, ] c, 0 state ! exit [ current @ @ dup c@ 96 or swap c! drop
Ich werde einige Erklärungen abgeben. ?state 110 ?pairs
Hier überprüfen wir, ob der Kompilierungsstatus wirklich festgelegt ist und 110 auf dem Stapel liegt. Andernfalls tritt versehentlich ein Interrupt auf. lit8 [ blword exit find cfa c@ c, ]
Dazu kompilieren wir den lit-Befehl mit dem Bytecode des exit-Befehls. Ich musste in den Ausführungsmodus wechseln, das Wort exit finden, die Ausführungsadresse abrufen und den Befehlscode von dort abrufen. All dies war erforderlich, da wir das Wort noch nicht kompiliert haben. Wenn es so wäre, würde es ausreichen, einfach "compile exit" zu schreiben :) c, 0 state !
Dadurch wird der Befehl exit kompiliert, wenn das Wort ";" ausgeführt wird, und anschließend wird der Interpretationsmodus festgelegt. Das Wort "[" kann hier nicht verwendet werden, da es das unmittelbare Vorzeichen hat und jetzt ausgeführt wird , aber wir müssen solche Befehle in das Wort ";" kompilieren , damit sie den Kompilierungsmodus deaktivieren. exit [
Das haben wir schon erlebt. Das Wort exit wird kompiliert und der Kompilierungsmodus ist deaktiviert. Alles, das Wort ";" zusammengestellt. Und was steht dort noch weiter geschrieben? current @ @ dup c@ 96 or swap c! drop
Sie müssen das unmittelbare Flag für das neue Wort setzen. Dies ist genau das, was die angegebene Sequenz tut, mit Ausnahme des Wortabfalls. Das Wort drop entfernt das vergessene 110, das das Wort ":" am Anfang der Schöpfung platziert hat.Jetzt ist alles!Wir starten und versuchen es. $ ./forth ( 0 ): > : ^3 dup dup * * ; ( 0 ): > 6 ^3 . 216 ( 0 ): >
Da ist! Dies ist das erste Wort, das unser Compiler "for real" kompiliert hat.Aber wir haben immer noch keine Bedingungen, keine Schleifen und vieles mehr ... Beginnen wir mit einem kleinen, aber sehr notwendigen Wort, um einen Compiler zu erstellen: sofort. Es legt das unmittelbare Attribut für das zuletzt erstellte Wort fest: : immediate current @ @ dup c@ 96 or swap c! ;
Eine vertraute Sequenz :) Vor kurzem wurde es manuell geschrieben, dies wird nicht mehr benötigt.Lassen Sie uns nun einige kleine, aber nützliche Wörter machen: : hex 16 base ! ; : decimal 10 base ! ; : bl 32 ; : tab 9 ; : lf 10 ;
Hex und Dezimal setzen das entsprechende Zahlensystem. Der Rest sind Konstanten zum Erhalten der entsprechenden Zeichencodes.Wir machen auch ein Wort für das Kopieren einer Zeile mit einem Zähler:: cmove over c @ 1+ move;Und jetzt werden wir uns mit Bedingungen beschäftigen. Wenn ein Wort kompiliert würde, würde es im Allgemeinen so aussehen: : if ?state compile ?nbranch8 here 0 c, 111 ; immediate : then ?state 111 ?pairs dup here swap - swap c! ; immediate
Alle diese Wörter am Anfang verifizieren, dass der Kompilierungsmodus eingestellt ist, und erzeugen einen Fehler, wenn dies nicht der Fall ist.Das if-Wort kompiliert eine bedingte Verzweigung, reserviert ein Byte für den Befehlsparameter der bedingten Verzweigung und schiebt die Adresse dieses Bytes auf den Stapel. Dann schiebt es den Steuerwert 111 auf den Stapel.Das Wort prüft dann, ob der Steuerwert 111 vorhanden ist, und schreibt dann den Versatz in die Adresse auf dem Stapel.Und sofort das Wort anders machen. Zu Beginn wird der bedingungslose Sprungbefehl kompiliert, um den else-Zweig zu umgehen. Auf die gleiche Weise, als ob der Übergangsversatz noch nicht bekannt wäre, wird er einfach reserviert und seine Adresse auf den Stapel geschoben. Nun, danach wird genau das Gleiche gemacht wie damals: Die Adresse des Catch-Übergangs wird auf den else-Zweig gesetzt. Etwas ist schwieriger zu beschreiben als der Code selbst :) Wenn jemand es gründlich herausfinden möchte, ist es besser, die Arbeit eines solchen maximal vereinfachten Codes zu analysieren: : if compile ?nbranch8 here 0 c, ; immediate : then dup here swap - swap c! ; immediate
Nun programmieren wir den echten Code. Da wir das Wort kompilieren nicht haben, wenden wir den gleichen Trick an wie beim Erstellen des Wortes ";": : if ?state lit8 [ blword ?nbranch8 find cfa c@ c, ] c, here 0 c, 111 ; immediate : then ?state 111 ?pairs dup here swap - swap c! ; immediate : else ?state 111 ?pairs lit8 [ blword branch8 find cfa c@ c, ] c, here 0 c, swap dup here swap - swap c! 111 ; immediate
Jetzt können Sie versuchen, die Bedingung zu kompilieren. Lassen Sie uns zum Beispiel ein Wort machen, das 1000 druckt, wenn sich 5 auf dem Stapel befinden, und 0 in anderen Fällen: $ ./forth ( 0 ): > : test 5 = if 1000 . else 0 . then ; ( 0 ): > 22 test 0 ( 0 ): > 3 test 0 ( 0 ): > 5 test 1000 ( 0 ): >
Es ist klar, dass ein solches Ergebnis nicht sofort funktioniert hat, es gab Fehler, es gab Debugging. Aber am Ende haben die Bedingungen funktioniert!Ein kleiner Exkurs über die Länge der Übergangsbefehle, , 127 . . , , . , , . 8 , 40 127 . , ?
. — 16 .
. 16 — . , , call, . , 11 ( 1023 ). 300 1000 . , . 3 , 8 . : (?nbranch), (?branch) (branch). — 24 .
Wir haben Bedingungen, das Leben wird einfacher :)Lassen Sie uns ein Wort sagen. "(Punkt-Zitat). Es zeigt den angegebenen Text an, wenn er ausgeführt wird. Es wird folgendermaßen verwendet: ." "
Sie können dieses Wort nur im Kompilierungsmodus verwenden. Dies wird deutlich, nachdem wir das Gerät dieses Wortes analysiert haben: : ." ?state 34 word dup if lit8 [ blword (.") find cfa c@ c, ] c, str, else drop then ; immediate
Dieses Wort wird im Kompilierungsmodus ausgeführt. Es dauert eine Zeichenfolge vom Eingabestream bis zu Anführungszeichen (34 Wörter). Wenn die Zeile nicht abgerufen werden konnte, wird nichts ausgeführt. Hier wäre es jedoch besser, eine Diagnose abzuleiten. Aber für die Ausgabe der Zeile ist dieses Wort genau das, was wir tun :) Wenn nötig, können Sie dieses Wort bereits mit Diagnose neu definieren.Wenn es möglich war, die Zeichenfolge abzurufen, wird der Byte-Befehl (. ") Kompiliert und dann die Zeichenfolge empfangen. Dieser Byte-Befehl (gepunktetes Anführungszeichen in Klammern) zeigt bei Ausführung die Zeichenfolge an, die hinter dem Befehlsbyte kompiliert wurde.Schau es dir an.
$ ./forth ( 0 ): > : test ." " ; ( 0 ): > test ( 0 ): >
Und zum Schluss lassen wir das Wort kompilieren.Es ist klar, dass dieses Wort im Kompilierungsmodus den Namen des nächsten Wortes aus dem Stream übernehmen sollte. Finden Sie ihn im Wörterbuch. Und dann gibt es Optionen: Es kann ein Byte-Befehl oder ein in Byte-Code geschriebenes Wort sein. Diese Wörter müssen auf unterschiedliche Weise zusammengestellt werden. Daher werden wir zwei Hilfswörter erstellen: "(compile_b)" und "(compile_c)".(compile_b) kompiliert den Aufrufbefehl, um den Bytecode aufzurufen. Der Parameter ist ein 64-Bit-Wort - die Adresse des aufgerufenen Bytecodes.(compile_c) kompiliert den Byte-Befehl. Dementsprechend ist der Parameter dieses Befehls ein Byte - der Befehlscode.Nun, das Wort compile selbst kompiliert entweder (compile_b) oder (compile_c) mit den entsprechenden Parametern.Beginnen wir mit (compile_c),wie beim einfachsten: : (compile_c) r> dup c@ swap 1+ >rc, ;
Trotz seiner Einfachheit schreiben wir zuerst ein Wort in Bytecode, der an sich Parameter hat. Deshalb werde ich kommentieren. Nach der Eingabe von (compile_c) befindet sich die Rücksprungadresse auf dem Rückgabestapel, da sie nicht banal ist. Dies ist die Adresse des nächsten Bytes nach dem Aufrufbefehl. Die Situation zum Zeitpunkt des Anrufs ist unten dargestellt. A0 - Aufrufbefehlscode, XX - Aufrufbefehlsparameter - Aufrufadresse (Offset) des Bytecodes des Wortes (compile_c).
Die Rücksprungadresse gibt das Byte NN an. Normalerweise gibt es den Code für das nächste Byte des Befehls. Aber unser Wort hat Parameter, also ist NN nur die Parameter des Wortes "(compile_c)", nämlich der Bytecode des kompilierten Befehls. Sie müssen dieses Byte lesen und die Rücksprungadresse ändern, indem Sie sie zum nächsten Bytebefehl weiterleiten. Dies geschieht durch die Sequenz „r> dup c @ swap 1+> r“. Diese Sequenz zieht die Rücksprungadresse vom Rückgabestapel zum regulären Stapel, ruft ein Byte daraus ab, fügt eins hinzu (Rücksprungadresse) und gibt es zurück zum Rückgabestapel zurück. Der verbleibende Befehl "c" kompiliert den aus den Parametern erhaltenen Bytebefehlscode.(compile_b) ist nicht viel komplizierter: : (compile_b) r> dup @ swap 8 + >r compile_b ;
Hier ist alles gleich, nur der 64-Bit-Parameter wird gelesen und das Wort compile_b wird verwendet, um das Wort zu kompilieren, das wir bereits für den Compiler erstellt haben.Und jetzt kompilieren Sie das Wort. Wie bereits erwähnt, liest es den Namen des Wortes, findet es und kompiliert einen der beiden vorherigen Befehle. Ich werde es nicht kommentieren, wir haben bereits alle verwendeten Konstruktionen angewendet und zerlegt.Word kompilieren : compile blword over over find dup if dup c@ 128 and if cfa c@ (compile_b) [ blword (compile_c) find cfa , ] c, else cfa (compile_b) [ blword (compile_b) find cfa , ] , then drop drop else drop ." compile: " type ." - not found" then ; immediate
Um das erstellte Wort zu überprüfen, machen wir mit seiner Hilfe das Wort ifnot. : ifnot ?state compile ?branch8 here 0 c, 111 ; immediate
Probieren Sie es aus!
$ ./forth ( 0 ): > : test 5 = ifnot 1000 . else 0 . then ; ( 0 ): > 22 test 1000 ( 0 ): > 3 test 1000 ( 0 ): > 5 test 0 ( 0 ): >
Alles ist in Ordnung! Und es ist Zeit, Zyklen zu machen ...In diesem Artikel werden wir Zyklen mit einer Bedingung machen. Das Fort hat zwei Möglichkeiten für einen Zyklus mit einer Bedingung.Die erste Option ist beginnen ... bis. Das Wort bis entfernt den Wert vom Stapel, und wenn er nicht gleich Null ist, endet der Zyklus.Die zweite Option ist beginnen ... während ... wiederholen. In diesem Fall erfolgt die Prüfung, wenn das Wort while ausgeführt wird. Die Schleife wird beendet, wenn der Wert auf dem Stapel Null ist.Die Zyklen auf der Festung werden auf die gleiche Weise wie die Bedingungen durchgeführt - unter bedingten und bedingungslosen Übergängen. Ich bringe den Code mit, Kommentare, denke ich, werden nicht benötigt. : begin ?state here 112 ; immediate : until ?state 112 ?pairs compile ?nbranch8 here - c, ; immediate : while ?state 112 ?pairs compile ?nbranch8 here 0 c, 113 ; immediate : repeat ?state 113 ?pairs swap compile branch8 here - c, dup here swap - swap c! ; immediate
Heute sind wir mit dem Compiler fertig. Es ist sehr wenig übrig. Von den Schlüsselfunktionen, die noch nicht implementiert wurden, sind nur Zyklen mit einem Zähler. Es lohnt sich auch, den Befehl exit loop zu verlassen. Wir werden es nächstes Mal tun.Den Zyklusbefehl haben wir aber nicht erlebt!Wir tun dies, indem wir die Standardwortwörter schreiben. Wir müssen endlich unser Wörterbuch sehen.Dazu machen wir zu Beginn das Wort link @. Das Kommunikationsfeld wird aus dem Wörterbucheintrag extrahiert (Versatz zum vorherigen Eintrag). Wie wir uns erinnern, kann das Kommunikationsfeld eine andere Größe haben: 1, 2, 4 oder 8 Bytes. Dieses Wort nimmt die Adresse des Wörterbucheintrags auf den Stapel und gibt zwei Werte zurück: die Adresse des Namensfelds und den Wert des Kommunikationsfelds. : link@ dup c@ 3 and swap 1+ swap dup 0= if drop dup 1+ swap c@ else dup 1 = if drop dup 2 + swap w@ else 2 = if drop dup 4 + swap i@ else drop dup 8 + swap @ then then then ;
Und jetzt können Sie das Wort Wörter machen: : words context @ @ 0 begin + dup link@ swap count type tab emit dup 0= until drop drop ;
Starten ... $ ./forth ( 0 ): > words words link@ repeat while until begin ifnot compile (compile_b) (compile_c) ." else then if cmove tab bl decimal hex immediate ; bye ?state ?pairs : str, interpret $compile compile_b compile_n compile_1248 compile_c c, w, i, , allot here h test_bv test_bvc [ ] state .s >in #tib tib . #> #s 60 # hold span holdpoint holdbuf base quit execute cfa find word blword var16 var8 (.") (") count emit expect type lshift rshift invert xor or and >= <= > < = 0> 0< 0= bfind compare syscall fill move rpick r@ r> >r -! +! i! i@ w! w@ c! c@ ! @ depth roll pick over -rot rot swap drop dup abs /mod mod / * - + 1+ 1- exit ?nbranch16 ?nbranch8 ?branch16 ?branch8 branch16 branch8 call8b0 call64 call32 call16 call8f lit64 lit32 lit16 lit8 8 4 3 2 1 0 context definitions current forth ( 0 ): >
Hier ist es, unser Reichtum :)Ich wollte alles sagen ... nein, machen wir es trotzdem möglich, eine Datei mit einem Fort-Programm zum Kompilieren und Ausführen als Parameter anzugeben.Wir machen Syscall-Befehle, um die Datei zu öffnen, zu schließen und zu lesen. Wir definieren die dafür notwendigen Konstanten. : file_open 0 0 0 2 syscall ; : file_close 0 0 0 0 0 3 syscall ; : file_read 0 0 0 0 syscall ; : file_O_RDONLY 0 ; : file_O_WRONLY 1 ; : file_O_RDWR 3 ;
Jetzt können Sie das Startwort _start setzen: : _start 0 pick 1 > if 2 pick file_O_RDONLY 0 file_open dup 0< if .\" error: \" . quit then dup here 32 + 32768 file_read dup 0< if .\" error: \" . quit then swap file_close drop #tib ! here 32 + tib ! 0 >in ! interpret then ;
Dieses Wort wird aus der Datei geladen und führt ein beliebiges Fort-Programm aus. Genauer gesagt führt der Interpreter alles aus, was in dieser Datei enthalten sein wird. Und es kann zum Beispiel eine Zusammenstellung neuer Wörter und deren Ausführung geben. Der Dateiname wird beim Start durch den ersten Parameter angezeigt. Ich werde nicht auf Details eingehen, aber die Startparameter unter Linux werden durch den Stapel geleitet. Das Wort _start erreicht sie mit den Befehlen 0 pick (Anzahl der Parameter) und 2 pick (Zeiger auf den ersten Parameter). Bei einem Fort-System liegen diese Werte außerhalb des Stapels, aber Sie können sie mit dem Befehl pick abrufen. Die Dateigröße ist auf 32 KB begrenzt, während keine Speicherverwaltung erfolgt.Jetzt muss am Ende noch in die fcode-Zeile geschrieben werden: _start quit
Erstellen Sie eine Datei test.f und schreiben Sie dort etwas auf die Festung. Zum Beispiel der euklidische Algorithmus zum Finden des größten gemeinsamen Faktors: : NOD begin over over <> while over over > if swap over - swap else over - then repeat drop ; 23101 44425 NOD . bye
Wir fangen an.
$ ./forth test.f 1777 Bye! $
Die Antwort ist richtig. Das Wort wurde zusammengestellt und dann erfüllt. Das Ergebnis wird angezeigt, dann wurde der Befehl bye ausgeführt. Wenn Sie die letzten beiden Zeilen entfernen, wird das Wort NOD zum Wörterbuch hinzugefügt und das System wechselt zur Befehlszeile. Sie können bereits Programme schreiben :-)Das ist alles.
Wen kümmert es, Sie können die Quelle oder die vorgefertigte Binärdatei für Linux auf x86-64 von Github herunterladen: https://github.com/hal9000cc/forth64Quellen werden mit einer Lizenz geliefert. GNU GPL v2 DCH v1 - Mach was du willst :-)