Wie funktioniert Zickzack?

Von einem Übersetzer: Dieser Beitrag wurde am 15. März 2018 im Blog des Autors veröffentlicht. Während sich eine Sprache weiterentwickelt, kann ihre Syntax derzeit unterschiedlich sein. Alles, was beschrieben wird, bezieht sich auf Zig 0.2.0, die aktuelle Version der Sprache ist Zig 0.3.0.

Ich habe den Autor des Beitrags kontaktiert und er hat freundlicherweise einen Link zum Repository mit der aktuellen Version der Projektquellen auf Zig 0.3.0 bereitgestellt

Hallo! Schreiben wir einen Brainfuck-Dolmetscher! "Warum?" "Sie können fragen, aber Sie werden die Antwort hier nicht finden."

Ich werde es auf Zig machen .

Zig ist ...


... eine neue Programmiersprache. Es befindet sich noch in der Beta und entwickelt sich rasant. Wenn Sie den Zig-Code schon einmal gesehen haben, scheint Ihnen der Code in diesem Beitrag etwas anders zu sein. Er ist wirklich anders! Zig 0.2.0 wurde gerade veröffentlicht, zeitgleich mit der Veröffentlichung von LLVM 6 vor einigen Wochen, und enthält viele Syntaxänderungen und allgemeine Sprachverbesserungen. Meistens wurden viele „Zaubersprüche“ durch Schlüsselwörter ersetzt. Hier finden Sie eine ausführlichere Erklärung aller Änderungen!

Zig ist lesbar und für diejenigen, die mit kompilierten und getippten Sprachen wie C, C ++ und an einigen Stellen Rust vertraut sind, relativ intuitiv.

Der Code wurde mit Zig 0.2.0 kompiliert und getestet, das derzeit über verschiedene Kanäle verfügbar ist, einschließlich Homebrew, wenn Sie unter OSX arbeiten: Brew Install Zig.

Fangen wir an


Informationen zur Funktionsweise von Brainfuck finden Sie hier . Dort gibt es fast nichts zu lernen, aber es ist eine Turing-vollständige Sprache, was bedeutet, dass Sie alles darauf schreiben können .

Ich habe den Code hier veröffentlicht , falls Sie das Endprodukt oder frühzeitige Commits sehen möchten.

Zig ist eine kompilierte Sprache. Wenn Sie ein Programm kompilieren, sollte die resultierende Binärdatei (wenn Sie eine ausführbare Binärdatei kompilieren, keine Bibliothek) eine Hauptfunktion haben, die den Einstiegspunkt markiert.

Also ...

// main.zig fn main() void { } 

... und fang an ...

 $ zig build-exe main.zig 

... gibt aus ...

 /zig/std/special/bootstrap.zig:70:33: error: 'main' is private /zigfuck/main.zig:2:1: note: declared here 

main muss als öffentlich deklariert werden, um außerhalb des Moduls sichtbar zu sein ...

 // main.zig pub fn main() void { } 

Lassen Sie das Brainfuck-Programm ein Array von 30.000 Bytes als Speicher verwenden, ich werde ein solches Array erstellen.

 // main.zig pub fn main() void { const mem: [30000]u8; } 

Ich kann eine Konstante (const) oder eine Variable (var) deklarieren. Hier habe ich mem als Array von 30.000 vorzeichenlosen (u) Bytes (8 Bits) deklariert.

Dies wird nicht kompiliert.

 /main.zig:3:5: error: variables must be initialized 

Ein äquivalentes C-Programm würde normal kompilieren: Ich kann eine Variable ohne Initialisierung deklarieren, aber Zig zwingt mich, jetzt eine Entscheidung zu treffen, wenn die Variable deklariert wird. Es ist mir vielleicht egal, was darin geschrieben wird, aber ich muss dies ausdrücklich angeben. Ich werde dies tun, indem ich die Variable mit einem undefinierten Wert (undefiniert) initialisiere.

 // main.zig pub fn main() void { const mem: [30000]u8 = undefined; } 

Die Initialisierung einer Variablen mit einem undefinierten Wert gibt keine Garantie für den Wert der Variablen im Speicher. Dies entspricht einer nicht initialisierten Variablendeklaration in C, außer dass Sie dies explizit angeben müssen.

Aber vielleicht ist es mir egal, wie ich diesen Speicher initialisiere. Vielleicht möchte ich eine Garantie haben, dass die Nullen oder ein beliebiger Wert dort geschrieben sind. In diesem Fall sollte ich auch ausdrücklich Folgendes angeben:

 // main.zig pub fn main() void { const mem = []u8{0} ** 30000; } 

Es mag seltsam erscheinen, aber ** ist der Operator, mit dem Arrays erweitert werden. Ich deklariere ein Array von 0 Bytes, erweitere es dann auf 30.000 und erhalte den endgültigen Initialisierungswert von 30.000 Null-Bytes. Diese Operation wird einmal zur Kompilierungszeit ausgeführt . comptime ist eine der großartigen Ideen von Zig, und ich werde in einem der folgenden Beiträge darauf zurückkommen.

Jetzt schreiben wir ein Programm über Brainfuck, das nichts anderes tut, als den ersten Speicherplatz fünfmal zu erhöhen!

 pub fn main() void { const mem = []u8{0} ** 30000; const src = "+++++"; } 

In Zig sind Strings Byte-Arrays. Ich sollte src nicht als Byte-Array deklarieren, da der Compiler dies impliziert. Dies ist optional, aber wenn Sie möchten, ist es möglich:

 const src: [5]u8 = "+++++"; 

Dies wird gut kompiliert. Dies ist jedoch:

 const src: [6]u8= "+++++"; 

wird nicht.

 main.zig:5:22: error: expected type '[6]u8', found '[5]u8' 

Noch ein Hinweis: Da Strings nur Arrays sind, enden sie nicht mit Null. Sie können jedoch eine nullterminierte Zeichenfolge C deklarieren. Als Literal sieht dies folgendermaßen aus:

 c"Hello I am a null terminated string"; 

Für das Gemeinwohl ...


Ich möchte mit jedem Zeichen in einer Zeichenfolge etwas tun. Ich kann es schaffen! Zu Beginn von main.zig importiere ich einige Funktionen aus der Standardbibliothek:

 const warn = @import("std").debug.warn; 

Der Import ist wie praktisch alles, was mit dem @ -Zeichen beginnt, eine integrierte Compilerfunktion . Solche Funktionen sind immer global verfügbar. Der Import hier funktioniert ähnlich wie bei Javascript. Sie können alles importieren, indem Sie in den Namespace graben und daraus öffentlich verfügbare Funktionen oder Variablen extrahieren. Im obigen Beispiel importiere ich die Warnfunktion direkt und ordne sie plötzlich der Warnkonstante zu. Jetzt kann sie angerufen werden. Dies ist ein gängiges Muster: Wir importieren direkt aus dem Standard-Namespace und rufen dann entweder std.debug.warn () auf oder weisen es der warn-Variablen zu. Es sieht so aus:

 const std = @import("std"); const warn = std.debug.warn; 

 const warn = @import("std").debug.warn; // main.zig pub fn main() void { const mem = []u8{0} ** 30000; const src = "+++++"; for (src) |c| { warn("{}", c); } } 

Während des Debuggens und der anfänglichen Entwicklung und des Testens möchte ich nur etwas auf dem Bildschirm drucken. Zig ist fehleranfällig und stdout ist auch fehleranfällig. Ich möchte dies jetzt nicht tun und kann mit warn, das wir aus der Standardbibliothek importiert haben, direkt auf stderr drucken.

warn nimmt eine formatierte Zeichenfolge an, wie printf in C! Der obige Code wird gedruckt:

 4343434343 

43 ist der ASCII-Zeichencode +. Ich kann auch schreiben:

 warn("{c}", c); 

und bekommen:

 +++++ 

Also haben wir den Speicherplatz initialisiert und das Programm geschrieben. Jetzt erkennen wir die Sprache selbst. Ich beginne mit + und ersetze den Körper der for-Schleife durch switch:

 for (src) |c| { switch(c) { '+' => mem[0] += 1 } } 

Ich erhalte zwei Fehler:

 /main.zig:10:7: error: switch must handle all possibilities switch(c) { ^ /main.zig:11:25: error: cannot assign to constant '+' => mem[0] += 1 ^ 

Natürlich kann ich einer Variablen, die eine Konstante ist, keinen neuen Wert zuweisen! mem muss eine Variable sein ...

 var mem = []u8{0} ** 30000; 

Wie bei anderen Fehlern sollte mein Schalterkonstrukt wissen, was zu tun ist, wenn das Zeichen nicht + ist, auch wenn nichts getan werden muss. In meinem Fall ist das genau das, was ich will. Ich fülle diesen Fall mit einem leeren Block:

 for (src) |c| { switch(c) { '+' => mem[0] += 1, else => {} } } 

Jetzt kann ich das Programm kompilieren. Rufen Sie am Ende warn auf und führen Sie Folgendes aus:

 const warn = @import("std").debug.warn; pub fn main() void { var mem = []u8{0} ** 30000; const src = "+++++"; for (src) |c| { switch(c) { '+' => mem[0] += 1, else => {} } } warn("{}", mem[0]); } 

Ich bekomme die Nummer 5 wie erwartet in stderr gedruckt.

Lass uns weitermachen ...


Ebenso unterstützen wir.

 switch(c) { '+' => mem[0] += 1, '-' => mem[0] -= 1, else => {} } 

Um> und <zu verwenden, müssen Sie eine zusätzliche Variable verwenden, die als "Zeiger" in dem Speicher dient, den ich für das Benutzer-Brainfuck-Programm zugewiesen habe.

 var memptr: u16 = 0; 

Da ein vorzeichenloses 16-Bit maximal 65535 sein kann, ist es mehr als ausreichend, 30.000 Byte Adressraum zu indizieren.

Tatsächlich würden 15 Bits für uns ausreichen, wodurch wir 32767 Bytes adressieren können. Zig erlaubt Typen mit unterschiedlichen Breiten , aber noch nicht u15.

Sie können u15 tatsächlich so machen:

 const u15 = @IntType(false, 15): 

Es wird vorgeschlagen, dass jeder [iu] \ d + -Typ als ganzzahliger Typ gültig ist.

Anstatt mem [0] zu verwenden, kann ich jetzt diese Variable verwenden.

 '+' => mem[memptr] += 1, '-' => mem[memptr] -= 1, 

<und> erhöht und dekrementiert diesen Zeiger einfach.

 '>' => memptr += 1, '<' => memptr -= 1, 

Großartig Wir können jetzt ein echtes Programm schreiben!

Überprüfen Sie 1,2,3


Zig hat einen eingebauten Testmotor. Überall in einer Datei kann ich einen Testblock schreiben:

 test "Name of Test" { // test code } 

und führen Sie den Test über die Befehlszeile aus: zig test $ FILENAME. Der Rest der Testblöcke entspricht dem regulären Code.

Schauen wir uns das an:

 // test.zig test "testing tests" {} zig test test.zig Test 1/1 testing tests...OK 

Ein leerer Test ist natürlich nutzlos. Ich kann assert verwenden, um die Ausführung der Tests tatsächlich zu bestätigen.

 const assert = @import("std").debug.assert; test "test true" { assert(true); } test "test false" { assert(false); } 

 zig test test.zig "thing.zig" 10L, 127C written :!zig test thing.zig Test 1/2 test true...OK Test 2/2 test false...assertion failure [37;1m_panic.7 [0m: [2m0x0000000105260f34 in ??? (???) [0m [37;1m_panic [0m: [2m0x0000000105260d6b in ??? (???) [0m [37;1m_assert [0m: [2m0x0000000105260619 in ??? (???) [0m [37;1m_test false [0m: [2m0x0000000105260cfb in ??? (???) [0m [37;1m_main.0 [0m: [2m0x00000001052695ea in ??? (???) [0m [37;1m_callMain [0m: [2m0x0000000105269379 in ??? (???) [0m [37;1m_callMainWithArgs [0m: [2m0x00000001052692f9 in ??? (???) [0m [37;1m_main [0m: [2m0x0000000105269184 in ??? (???) [0m [37;1m??? [0m: [2m0x00007fff5c75c115 in ??? (???) [0m [37;1m??? [0m: [2m0x0000000000000001 in ??? (???) [0m 

Der Test fiel. Verwenden Sie den folgenden Befehl, um den Fehler zu reproduzieren:

 ./zig-cache/test 

Die Stapelspur auf der Mohnblume befindet sich noch in der Entwicklung.

Um dies effizient zu testen, muss ich es in Stücke zerbrechen. Beginnen wir damit:

 fn bf(src: []const u8, mem: [30000]u8) void { var memptr: u16 = 0; for (src) |c| { switch(c) { '+' => mem[memptr] += 1, '-' => mem[memptr] -= 1, '>' => memptr += 1, '<' => memptr -= 1, else => {} } } } pub fn main() void { var mem = []u8{0} ** 30000; const src = "+++++"; bf(src, mem); } 

Es sollte scheinen zu funktionieren, oder?

Aber ...

 /main.zig:1:29: error: type '[30000]u8' is not copyable; cannot pass by value 

Dies wird unter https://github.com/zig-lang/zig/issues/733 beschrieben .

Zig ist diesbezüglich streng. Komplexe Typen und alle Objekte, deren Größe geändert werden kann, können nicht als Wert übergeben werden. Dies macht die Stapelzuordnung vorhersehbar und logisch und vermeidet unnötiges Kopieren. Wenn Sie die Semantik der Wertübertragung in Ihrem Programm verwenden möchten, können Sie sie mithilfe Ihrer Zuordnungsstrategie selbst implementieren. Die Sprache selbst unterstützt dies jedoch unter normalen Umständen nicht.

Der natürliche Weg, um diese Einschränkung zu umgehen, besteht darin, einen Zeiger anstelle eines Werts zu übergeben (als Referenz übergeben). Zig verwendet eine andere Strategie, Scheiben. Ein Slice ist ein Zeiger mit einer Länge und einem Häkchen für das Fallen in Ränder. Die Syntax in der Funktionssignatur sieht folgendermaßen aus:

 fn bf(src: []const u8, mem: []u8) void { ... } 

und beim Aufrufen der Funktion sieht es so aus:

 bf(src, mem[0..mem.len]); 

Beachten Sie, dass ich die Obergrenze einfach durch Bezugnahme auf die Länge des Arrays definiert habe. Für solche Fälle gibt es eine abgekürzte Notationsform:

 bf(src, mem[0..]); 

Jetzt kann ich Tests schreiben, die die Funktion bf () direkt testen. Ich werde vorerst Testfunktionen am Ende der Datei hinzufügen ...

 test "+" { var mem = []u8{0}; const src = "+++"; bf(src, mem[0..]); assert(mem[0] == 3); } 

Ich nehme das mem-Array von einem Byte und überprüfe dann, was passieren soll (das Byte wird dreimal inkrementiert). Es funktioniert!

 Test 1/1 +...OK 

"-" wird auf die gleiche Weise überprüft:

 test "-" { var mem = []u8{0}; const src = "---"; bf(src, mem[0..]); assert(mem[0] == 253); } 

Das ____ funktioniert nicht! Wenn ich versuche, 1 von 0 zu subtrahieren, bekomme ich ...

 Test 2/2 -...integer overflow 

mem ist ein Array von vorzeichenlosen Bytes, und das Subtrahieren von 1 von 0 führt zu einem Überlauf. Wieder lässt mich Zig explizit erklären, was ich will. In diesem Fall muss ich mir keine Sorgen um einen Überlauf machen, ich möchte, dass dies geschieht, da es sich um modulare Arithmetik gemäß der Spezifikation von Brainfuck handelt . Dies bedeutet, dass das Dekrementieren einer Zelle mit der Nummer 0 255 ergibt und ein Inkrementieren von 255 0 ergibt.

Zig verfügt über mehrere Hilfsarithmetikoperationen, die die Semantik eines garantierten „Umhüllens“ bieten.

 '+' => mem[memptr] +%= 1, '-' => mem[memptr] -%= 1, 

Dies löst das gesamte Überlaufproblem und macht das, was ich erwartet hatte.

Zum Testen von <und> navigiere ich durch ein kleines Array und überprüfe den Wert der inkrementierten Zelle:

 test ">" { var mem = []u8{0} ** 5; const src = ">>>+++"; bf(src, mem[0..]); assert(mem[3] == 3); } 

und ...

 test "<" { var mem = []u8{0} ** 5; const src = ">>>+++<++<+"; bf(src, mem[0..]); assert(mem[3] == 3); assert(mem[2] == 2); assert(mem[1] == 1); } 

Im letzteren Fall kann ich das Ergebnis direkt mit einem statischen Array vergleichen, indem ich ...

 const mem = std.mem; 

Denken Sie daran, dass ich bereits std importiert habe. Im folgenden Beispiel verwende ich mem.eql in diesem Namespace:

 test "<" { var storage = []u8{0} ** 5; const src = ">>>+++<++<+"; bf(src, storage[0..]); assert(mem.eql(u8, storage, []u8{ 0, 1, 2, 3, 0 })); } 

... und denken Sie daran, String-Literale, dies sind nur U8-Arrays im Zickzack, und ich kann hexadezimale Literale in sie einfügen, d. h. Der folgende Code funktioniert genauso!

 assert(mem.eql(u8, storage, "\x00\x01\x02\x03\x00")); 

Fügen Sie das "." Hinzu! Der Bytewert in der Zelle, auf die der Zeiger zeigt, wird einfach als Zeichen gedruckt. Ich verwende jetzt warn, aber später werde ich es durch stdout ersetzen. Dies ist konzeptionell einfach, aber in der Implementierung etwas verwirrt. Ich werde es später tun!

 '.' => warn("{c}", storage[memptr]), 

Zyklen
[und] - hier beginnt die Magie ....

[- Wenn der Wert der aktuellen Zelle Null ist, überspringen Sie die Schritte zur schließenden Klammer, ohne den Code auszuführen.
] - Wenn der Wert der aktuellen Zelle nicht Null ist, kehren Sie zur öffnenden Klammer zurück und führen Sie den Code erneut aus.

Dieses Mal werde ich mit einem Test beginnen, ich werde sie zusammen testen (offensichtlich macht es keinen Sinn, sie separat zu testen). Der erste Testfall - die Speicherzelle [2] sollte leer sein, obwohl die Schleife sie erhöhen sollte, wenn sie startet:

 test "[] skips execution and exits" { var storage = []u8{0} ** 3; const src = "+++++>[>+++++<-]"; bf(src, storage[0..]); assert(storage[0] == 5); assert(storage[1] == 0); assert(storage[2] == 0); } 

und ich werde Leerzeichen für die switch-Anweisung erstellen:

 '[' => if (storage[memptr] == 0) { }, ']' => if (storage[memptr] == 0) { }, 

Was ist jetzt zu tun? Sie können einen naiven Ansatz verwenden. Ich erhöhe nur den src-Zeiger, bis ich ihn finde]. Ich kann die for-Schleife hierfür jedoch nicht im Zickzack verwenden. Sie wurde nur zum Durchlaufen von Sammlungen erstellt, ohne dass deren Elemente fehlen. Ein geeignetes Konstrukt hier ist während:

war:

 var memptr: u16 = 0; for (src) |c| { switch(c) { ... } } 

wurde ...

 var memptr: u16 = 0; var srcptr: u16 = 0; while (srcptr < src.len) { switch(src[srcptr]) { ... } srcptr += 1; } 

Jetzt kann ich den srcptr-Zeiger in der Mitte des Blocks neu zuweisen. Ich werde dies tun:

 '[' => if (storage[memptr] == 0) { while (src[srcptr] != ']') srcptr += 1; }, 

Dies erfüllt den Test "[] überspringt die Codeausführung und wird beendet"
Dies erfüllt den Test "[] überspringt die Ausführung und beendet", obwohl er nicht ganz zuverlässig ist, wie wir sehen werden.

Was ist mit dem Schließen von Klammern? Ich glaube, es kann einfach analog geschrieben werden:

 test "[] executes and exits" { var storage = []u8{0} ** 2; const src = "+++++[>+++++<-]"; bf(src, storage[0..]); assert(storage[0] == 0); assert(storage[1] == 25); } ']' => if (storage[memptr] != 0) { while (src[srcptr] != '[') srcptr -= 1; }, 

Sie können sehen, was passiert ... Eine naive Lösung mit zwei Klammern weist einen schwerwiegenden Fehler auf und bricht in verschachtelten Schleifen vollständig ab. Beachten Sie Folgendes:

 ++>[>++[-]++<-] 

Das Ergebnis sollte {2, 0} sein, aber die erste offene Klammer bewegt sich einfach dumm zur ersten schließenden Klammer, und alles wird chaotisch. Sie müssen zur nächsten schließenden Klammer auf derselben Verschachtelungsebene springen. Es ist einfach, einen Tiefenzähler hinzuzufügen und ihn zu verfolgen, während Sie sich entlang der Linie vorwärts bewegen. Wir machen es in beide Richtungen:

 '[' => if (storage[memptr] == 0) { var depth:u16 = 1; srcptr += 1; while (depth > 0) { srcptr += 1; switch(src[srcptr]) { '[' => depth += 1, ']' => depth -= 1, else => {} } } }, ']' => if (storage[memptr] != 0) { var depth:u16 = 1; srcptr -= 1; while (depth > 0) { srcptr -= 1; switch(src[srcptr]) { '[' => depth -= 1, ']' => depth += 1, else => {} } } }, 

und verwandte Tests: Beachten Sie, dass src in beiden Tests eine innere Schleife enthält.

 test "[] skips execution with internal braces and exits" { var storage = []u8{0} ** 2; const src = "++>[>++[-]++<-]"; try bf(src, storage[0..]); assert(storage[0] == 2); assert(storage[1] == 0); } test "[] executes with internal braces and exits" { var storage = []u8{0} ** 2; const src = "++[>++[-]++<-]"; try bf(src, storage[0..]); assert(storage[0] == 0); assert(storage[1] == 2); } 

Beachten Sie separat [-] - die Redewendung von Brainfuck, was "Null dieser Zelle" bedeutet. Sie können sehen, dass es keine Rolle spielt, welchen Wert die Zelle zu Beginn hatte. Sie wird dekrementiert, bis sie 0 erreicht, und die Ausführung wird fortgesetzt.

Unglücklicher Weg


Ich habe nicht mit der Möglichkeit gerechnet, dass das Programm auf bf kaputt gehen würde. Was passiert, wenn ich meinem Dolmetscher ein falsches Eingabeprogramm sende? Zum Beispiel einfach [ohne schließende Klammer oder <, was sofort über das Speicherarray hinausgeht? (Ich kann den Speicherzeiger umbrechen, aber es ist besser, dies als Fehler zu betrachten).

Ich werde ein wenig nach vorne schauen und alle Unterschiede im Code erklären. Ich werde die bf-Interpreter-Funktion in eine separate Datei einfügen und auch die Funktionen seekBack und seekForward in meine eigenen kleinen Funktionen einfügen.

 const warn = @import("std").debug.warn; const sub = @import("std").math.sub; fn seekBack(src: []const u8, srcptr: u16) !u16 { var depth:u16 = 1; var ptr: u16 = srcptr; while (depth > 0) { ptr = sub(u16, ptr, 1) catch return error.OutOfBounds; switch(src[ptr]) { '[' => depth -= 1, ']' => depth += 1, else => {} } } return ptr; } fn seekForward(src: []const u8, srcptr: u16) !u16 { var depth:u16 = 1; var ptr: u16 = srcptr; while (depth > 0) { ptr += 1; if (ptr >= src.len) return error.OutOfBounds; switch(src[ptr]) { '[' => depth += 1, ']' => depth -= 1, else => {} } } return ptr; } pub fn bf(src: []const u8, storage: []u8) !void { var memptr: u16 = 0; var srcptr: u16 = 0; while (srcptr < src.len) { switch(src[srcptr]) { '+' => storage[memptr] +%= 1, '-' => storage[memptr] -%= 1, '>' => memptr += 1, '<' => memptr -= 1, '[' => if (storage[memptr] == 0) srcptr = try seekForward(src, srcptr), ']' => if (storage[memptr] != 0) srcptr = try seekBack(src, srcptr), '.' => warn("{c}", storage[memptr]), else => {} } srcptr += 1; } } 

Dies macht es meiner Meinung nach viel einfacher, den Schalter zu lesen. SearchForward und seekBack funktionieren und sehen sehr ähnlich aus, und ich war versucht, sie in etwas intelligenteres und kompakteres umzuwandeln, aber am Ende machen sie verschiedene Dinge und behandeln Fehler auch auf verschiedene Arten. Einfacher zu kopieren und anzupassen, so wird es klarer. Ich werde seekForward auch später anpassen, irgendwann, möglicherweise in einem nachfolgenden Beitrag.

Ich habe einige wichtige Dinge hinzugefügt! Beachten Sie, dass alle drei Funktionen jetzt einen Typ zurückgeben! Dies ist die neue Syntax für den früheren Typ% T (Fehlerunion). Dies bedeutet, dass die Funktion entweder einen bestimmten Typ oder einen Fehler zurückgeben kann. Wenn ich versuche, eine solche Funktion aufzurufen, muss ich entweder try verwenden, bevor ich die Funktion aufrufe, die den Fehler in den Aufrufstapel wirft, wenn der Fehler auftritt, oder catch verwenden:

 const x = functionCall() catch {} 

Wo ich Fehler in einem Catch-Block behandle. Wie geschrieben, kann catch Fehler verschlucken. Dies ist eine schlechte Praxis, aber hier lässt Zig uns dies explizit tun. Wenn ich einen Fehler in einem leeren Block abfange, gebe ich an, dass ich entweder nicht glaube, dass ein Fehler auftreten kann, oder dass ich ihn nicht behandeln muss. In der Praxis kann es so etwas wie TODO sein, und tatsächlich ist es sehr einfach, es auch explizit zu machen!

 const x = functionCall() catch { @panic("TODO") } 

Denken Sie daran, dass ein solcher Fall im Produktionscode niemals auftreten wird. Ich benachrichtige den Compiler, dass ich weiß, was ich tue. Wenn ein Fehler auftreten könnte, müsste ich die Fehlerbehandlung hinzufügen.

Welche Fehler sollte ich von seekBack oder seekForward zurückgeben?

In seekBack:

 ptr = sub(u16, ptr, 1) catch return error.OutOfBounds; 

Ich habe den Dekrementzeiger ersetzt, um die Unterfunktion von std lib zu verwenden, die einen Überlauffehler auslöst, wenn ein Überlauf auftritt. Ich möchte diesen Fehler abfangen und stattdessen den OutOfBounds-Fehler zurückgeben, den ich hier nur mit ihm erstelle.

Fehler Zig ist im Grunde ein Array von Fehlercodes, die vom Compiler generiert werden, wenn Sie Fehler verwenden. Eine Art Fehler. Sie sind garantiert eindeutig und können als Werte in einem Schaltblock verwendet werden.

Ich möchte hier OutOfBounds verwenden, da ich semantisch, wenn der Speicherzeiger kleiner als Null wird, die Laufzeit auffordere, über den von mir zugewiesenen Speicherplatz hinauszugehen.

ähnlich in der Funktion seekForward:

 if (ptr >= src.len) return error.OutOfBounds; 

In diesem Fall, wenn der Zeiger größer als src.len ist, fange ich den Fehler hier ab und gebe den gleichen Fehler zurück.

beim Anruf:

 '[' => if (storage[memptr] == 0) srcptr = try seekForward(src, srcptr), ']' => if (storage[memptr] != 0) srcptr = try seekBack(src, srcptr), 

Ich versuche diese Funktionen aufzurufen. Wenn sie erfolgreich aufgerufen werden, werden sie korrekt ausgeführt und versuchen, srcptr zurückzugeben. Wenn sie nicht erfolgreich sind, beendet try die Funktion und gibt einen Fehler an den Ort des Aufrufs an die gesamte Funktion bf zurück.

Der Anruf kann von main sein!

 const bf = @import("./bf.zig").bf; // yes, hello const hello_world = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>."; pub fn main() void { storage = []u8{0} ** 30000; bf(hello_world, storage[0..]) catch {}; } 

Ich schlucke diesen Fehler hier und er sollte nicht gemacht werden, aber wir werden einen wichtigen Punkt darüber beachten, wie leicht Zick-Zack-Fehler den Aufrufstapel weiterleiten können. Es liegt nicht in der Verantwortung der aufrufenden Funktion, jeden Fehlerfall zu überprüfen, aber der Compiler erzwingt den Aufruf jeder Funktion, die bei einem Versuch fehlschlagen kann. Dies muss immer geschehen, auch wenn Fehler ignoriert werden!

Die neue Try / Catch-Syntax eliminiert die vielen Zaubersprüche wie %% und%, die die Leute nicht so sehr mögen.

Jetzt habe ich 7 von 8 Brainfuck-Charakteren implementiert, und dies reicht aus, um ein „aussagekräftiges“ Programm auszuführen.

Ein sinnvolles Programm


Hier ist das Programm:

 //   ,   const fib = "++++++++++++++++++++++++++++++++++++++++++++>++++++++++++++++++++++++++++++++>++++++++++++++++>>+<<[>>>>++++++++++<<[->+>-[>+>>]>[+[-<+>]>+>>]<<<<<<]>[<+>-]>[-]>>>++++++++++<[->-[>+>>]>[+[-<+>]>+>>]<<<<<]>[-]>>[++++++++++++++++++++++++++++++++++++++++++++++++.[-]]<[++++++++++++++++++++++++++++++++++++++++++++++++.[-]]<<<++++++++++++++++++++++++++++++++++++++++++++++++.[-]<<<<<<<.>.>>[>>+<<-]>[>+<<+>-]>[<+>-]<<<-]<<++..."; 

Lass uns rennen ...

 pub fn main() void { storage = []u8{0} ** 30000; bf(fib, storage[0..]) catch {}; } 

voila!

 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 121, 98, 219, 

Jedes Mal, wenn ich an eine Fibonacci-Serie denke, fällt mir eine Erinnerung ein ... Ich habe es in den 80er Jahren im PBS-Programm (Public Broadcasting Service, ein nichtkommerzieller amerikanischer Fernsehsender) erfahren, und ich erinnere mich immer daran. Ich dachte, es wäre vergessen, aber Youtube ist eine großartige Sache .

Wie kann ich das verbessern?


Ich habe bereits einige TODOs angedeutet. Ich hätte stderr nicht für die Ausgabe verwenden sollen. Ich möchte stdout verwenden.

Jedes Mal, wenn ich den Interpreter öffne, öffne ich den Stream in stdout und drucke ihn aus:

 const io = std.io; ... pub fn bf(src: []const u8, storage: []u8) !void { const stdout = &(io.FileOutStream.init(&(io.getStdOut() catch unreachable)).stream); ... '.' => stdout.print("{c}", storage[memptr]) catch unreachable, ... 

Was ist hier los? io.getStdOut(), ( catch unreachable — , !). , , , print. print , warn, . print , .

, stdout, stdout. Zig , , .

, , ? , , ? , ? , Zig !

, !

 const bf = @import("./bf.zig").bf; const warn = @import("std").debug.warn; const serpinsky = "++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[ -<<<[ ->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<< ]>.>+[>>]>+ ] "; pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch unreachable; } 


, bf , !void. , main. , :

 const bf = @import("./bf.zig").bf; const warn = @import("std").debug.warn; const serpinsky = "++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[ -<<<[ ->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<< ]>.>+[>>]>+ ] "; pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch |err| switch (err) { }; } 

!

 /Users/jfo/code/zigfuck/main.zig:7:46: error: error.OutOfBounds not handled in switch shell returned 1 

, bf ! , , stdout, bf. , , , try. , , , catch, try, , .

, :

 const io = std.io; ... pub fn bf(src: []const u8, storage: []u8) !void { const stdout = &(io.FileOutStream.init(&(io.getStdOut() catch unreachable)).stream); ... '.' => stdout.print("{c}", storage[memptr]) catch unreachable, ... 

:

 const io = std.io; ... pub fn bf(src: []const u8, storage: []u8) !void { const stdout = &(io.FileOutStream.init(&(try io.getStdOut())).stream); ... '.' => try stdout.print("{c}", storage[memptr]), ... 

:

 const bf = @import("./bf.zig").bf; const warn = @import("std").debug.warn; const serpinsky = "++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[ -<<<[ ->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<< ]>.>+[>>]>+ ] "; pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch |err| switch (err) { }; } 

, , !

 /Users/jfo/code/zigfuck/main.zig:7:46: error: error.SystemResources not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.OperationAborted not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.IoPending not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.BrokenPipe not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.Unexpected not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.WouldBlock not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.FileClosed not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.DestinationAddressRequired not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.DiskQuota not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.FileTooBig not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.InputOutput not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.NoSpaceLeft not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.AccessDenied not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.OutOfBounds not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.NoStdHandles not handled in switch shell returned 1 

Zig , ! switch , , , , .

 pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch |err| switch (err) { error.OutOfBounds => @panic("Out Of Bounds!"), else => @panic("IO error") }; } 

- , , , Zig, ! , ! !

Todo


, ! , , ",", brainfuck- getc, . , bf. , , Zig. , , , .

Fazit


, , Zig . Zig , , , , , ++. , , . , , . Zig , , , .

Zig, , 0.2.0 ! , , debug-, , ! --release-fast --release-safe, . .

Zig. , 1.0.0, Zig, , , !

, #zig freenode , .

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


All Articles