Bagaimana cara kerja zig?

Dari penerjemah: Posting ini diterbitkan di blog penulis pada 15 Maret 2018. Seiring berkembangnya bahasa, sintaksisnya mungkin berbeda saat ini. Segala sesuatu yang dijelaskan berhubungan dengan Zig 0.2.0, versi bahasa saat ini adalah Zig 0.3.0.

Saya menghubungi penulis posting, dan dia dengan ramah memberikan tautan ke repositori dengan versi terkini dari sumber proyek pada Zig 0.3.0

Halo Mari kita menulis penerjemah Brainfuck! "Kenapa?" "Anda mungkin bertanya, tetapi Anda tidak akan menemukan jawabannya di sini."

Saya akan melakukannya di Zig .

Zig adalah ...


... bahasa pemrograman baru. Masih dalam versi beta dan sedang berkembang pesat. Jika Anda pernah melihat kode Zig sebelumnya, kode dalam posting ini mungkin tampak sedikit berbeda dengan Anda. Dia benar-benar berbeda! Zig 0.2.0 baru saja dirilis, bertepatan dengan rilis LLVM 6 beberapa minggu yang lalu, dan mencakup banyak perubahan sintaks dan peningkatan bahasa secara umum. Sebagian besar, banyak "mantra" telah digantikan oleh kata kunci. Lihat di sini untuk penjelasan lebih lanjut tentang semua perubahan!

Zig dirancang agar dapat dibaca , dan relatif intuitif bagi mereka yang terbiasa dengan bahasa yang dikompilasi dan diketik seperti C, C ++, dan, di beberapa titik, Rust.

Kode ini dikompilasi dan diuji dengan Zig 0.2.0, yang tersedia sekarang melalui berbagai saluran , termasuk homebrew, jika Anda menggunakan OSX: brew install zig.

Mari kita mulai


Untuk mempelajari cara Brainfuck bekerja, lihat di sini . Hampir tidak ada yang bisa dipelajari di sana, tetapi bahasa Turing-lengkap , yang berarti Anda dapat menulis apa pun di sana.

Saya memposting kode di sini , jika Anda ingin melihat produk akhir atau komitmen awal.

Zig adalah bahasa yang dikompilasi. Ketika Anda mengkompilasi program, biner yang dihasilkan (jika Anda mengkompilasi biner yang dapat dieksekusi, bukan perpustakaan) harus memiliki fungsi utama yang menandai titik masuk.

Jadi ...

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

... dan mulai ...

 $ zig build-exe main.zig 

... membagikan ...

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

utama harus dideklarasikan sebagai publik agar dapat terlihat di luar modul ...

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

Biarkan program brainfuck menggunakan array 30.000 byte sebagai memori, saya akan membuat array seperti itu.

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

Saya dapat mendeklarasikan konstanta (const) atau variabel (var). Di sini, saya menyatakan mem sebagai array dari 30.000 byte (u) byte (8 bit) yang tidak ditandatangani.

Ini tidak dikompilasi.

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

Program C yang setara akan dikompilasi secara normal: Saya dapat mendeklarasikan variabel tanpa inisialisasi, tetapi Zig memaksa saya untuk membuat keputusan sekarang, pada saat variabel tersebut dideklarasikan. Saya mungkin tidak peduli apa yang akan ditulis di dalamnya, tetapi saya harus secara eksplisit menunjukkan ini. Saya akan melakukan ini dengan menginisialisasi variabel dengan nilai yang tidak ditentukan (undefined).

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

Inisialisasi variabel dengan nilai yang tidak ditentukan tidak memberikan jaminan tentang nilai variabel dalam memori. Ini sama dengan deklarasi variabel yang tidak diinisialisasi dalam C kecuali bahwa Anda perlu secara eksplisit menunjukkan ini.

Tapi mungkin saya tidak peduli bagaimana menginisialisasi memori ini. Mungkin saya ingin memiliki jaminan bahwa nol atau nilai sewenang-wenang ditulis di sana. Dalam hal ini, saya juga harus secara eksplisit menyatakan ini:

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

Mungkin terlihat aneh, tetapi ** adalah operator yang digunakan untuk memperluas array. Saya mendeklarasikan array 0 byte, dan kemudian memperluasnya menjadi 30.000, dan mendapatkan nilai inisialisasi akhir sebesar 30.000 byte nol. Operasi ini terjadi sekali, pada waktu kompilasi . comptime adalah salah satu ide hebat Zig, dan saya akan kembali padanya di salah satu posting berikut.

Sekarang mari kita menulis sebuah program di brainfuck yang tidak menambah slot memori pertama lima kali!

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

Di Zig, string adalah byte array. Saya seharusnya tidak mendeklarasikan src sebagai byte array, karena kompiler menyiratkan ini. Ini opsional, tetapi jika Anda mau, itu mungkin:

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

Ini akan dikompilasi dengan baik. Namun, ini:

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

tidak akan.

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

Satu lagi catatan: karena string hanya array, mereka tidak berakhir dengan nol. Namun, Anda dapat mendeklarasikan string diakhiri-nol C. Sebagai literal, itu akan terlihat seperti ini:

 c"Hello I am a null terminated string"; 

Demi kebaikan bersama ...


Saya ingin melakukan sesuatu dengan setiap karakter dalam sebuah string. Saya bisa melakukannya! Di awal main.zig, saya mengimpor beberapa fungsi dari pustaka standar:

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

impor , seperti hampir semua yang dimulai dengan tanda @, adalah fungsi kompiler bawaan . Fitur seperti itu selalu tersedia secara global. Impor di sini berfungsi mirip dengan javascript - Anda dapat mengimpor apa pun dengan menggali namespace dan mengekstraknya dari fungsi atau variabel apa pun yang tersedia untuk umum. Dalam contoh di atas, saya secara langsung mengimpor fungsi warn dan menetapkannya, secara tiba-tiba, ke konstanta peringatan. Sekarang dia bisa dipanggil. Ini adalah pola umum: kami mengimpor langsung dari namespace std dan kemudian memanggil std.debug.warn () atau menetapkannya ke variabel warn. Ini terlihat seperti ini:

 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); } } 

Selama debugging dan pengembangan serta pengujian awal, saya hanya ingin mencetak sesuatu di layar. Zig rawan kesalahan , dan stdout juga rawan kesalahan. Saya tidak ingin melakukan ini sekarang, dan saya dapat mencetak langsung ke stderr menggunakan warn, yang kami impor dari perpustakaan standar.

warnai mengambil string yang diformat, seperti printf di C! Kode di atas akan dicetak:

 4343434343 

43 adalah kode karakter ascii +. Saya juga bisa menulis:

 warn("{c}", c); 

dan dapatkan:

 +++++ 

Jadi, kami menginisialisasi ruang memori, dan menulis programnya. Sekarang kami menyadari bahasa itu sendiri. Saya akan mulai dengan +, dan mengganti tubuh for for dengan switch:

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

Saya mendapatkan dua kesalahan:

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

Tentu saja, saya tidak bisa menetapkan nilai baru ke variabel, yang merupakan konstanta! mem perlu dibuat variabel ...

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

seperti kesalahan lainnya, konstruksiku harus tahu apa yang harus dilakukan jika karakternya bukan +, bahkan jika tidak ada yang perlu dilakukan. Dalam kasus saya, inilah yang saya inginkan. Saya mengisi kasing ini dengan balok kosong:

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

Sekarang saya bisa mengkompilasi program. Panggil peringatan di akhir dan jalankan:

 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]); } 

Saya mendapatkan nomor 5 dicetak di stderr , seperti yang saya harapkan.

Mari kita lanjutkan ...


Demikian pula, kami mendukung.

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

Untuk menggunakan> dan <, Anda perlu menggunakan variabel tambahan, yang berfungsi sebagai "penunjuk" dalam memori yang saya alokasikan untuk program brainfuck pengguna.

 var memptr: u16 = 0; 

Karena 16-bit yang tidak ditandatangani dapat maksimum 65535, itu lebih dari cukup untuk mengindeks 30.000 byte ruang alamat.

pada kenyataannya, 15 bit akan cukup bagi kita, yang memungkinkan kita untuk mengatasi 32.767 byte. Zig memungkinkan jenis dengan lebar yang berbeda , tetapi belum u15.

Anda benar-benar dapat melakukan u15 dengan cara ini:

 const u15 = @IntType(false, 15): 

Diusulkan agar semua tipe [iu] \ d + valid sebagai tipe integer.

Sekarang alih-alih menggunakan mem [0], saya bisa menggunakan variabel ini.

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

<and> cukup tambahkan dan kurangi pointer ini.

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

Bagus Kita dapat menulis program nyata sekarang!

Periksa 1,2,3


Zig memiliki mesin uji bawaan. Di mana saja dalam file apa pun saya dapat menulis blok uji:

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

dan jalankan tes dari baris perintah: zig test $ FILENAME. Sisa blok uji sama dengan kode biasa.

Mari kita lihat ini:

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

Tentu saja, tes kosong tidak berguna. Saya dapat menggunakan menegaskan untuk benar-benar mengkonfirmasi pelaksanaan tes.

 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 

Tes jatuh. Gunakan perintah berikut untuk mereproduksi kesalahan:

 ./zig-cache/test 

Jejak tumpukan pada poppy masih dalam pengembangan.

Untuk mengujinya dengan efisien, saya perlu memecahnya menjadi beberapa bagian. Mari kita mulai dengan ini:

 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); } 

Seharusnya itu berfungsi, bukan?

Tapi ...

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

ini dijelaskan di https://github.com/zig-lang/zig/issues/733 .

Zig sangat tegas soal ini. Jenis yang kompleks, dan semua objek yang dapat diubah ukurannya, tidak bisa dilewati oleh nilai. Ini membuat alokasi tumpukan dapat diprediksi dan logis, dan menghindari penyalinan yang tidak perlu. Jika Anda ingin menggunakan semantik transfer berdasarkan nilai dalam program Anda, Anda dapat menerapkannya sendiri menggunakan strategi alokasi Anda, tetapi bahasa itu sendiri tidak mendukung hal ini dalam keadaan biasa.

Cara alami untuk mengatasi batasan ini adalah dengan melewatkan pointer daripada nilai (pass by reference). Zig menggunakan strategi yang berbeda, irisan. Sepotong adalah pointer dengan panjang yang melekat padanya dan dengan cek untuk jatuh ke perbatasan. Sintaks dalam tanda tangan fungsi terlihat seperti ini:

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

dan ketika memanggil fungsi itu terlihat seperti ini:

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

Perhatikan bahwa saya mendefinisikan batas atas hanya dengan merujuk pada panjang array. Ada bentuk notasi singkat untuk kasus-kasus seperti:

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

Sekarang saya dapat mulai menulis tes yang menguji fungsi bf () secara langsung. Saya akan menambahkan fungsi tes ke akhir file untuk saat ini ...

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

Saya mengambil array mem dari satu byte dan kemudian memeriksa apa yang harus terjadi (byte bertambah tiga kali). Itu berhasil!

 Test 1/1 +...OK 

"-" dicentang dengan cara yang sama:

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

Tidak bekerja! Ketika saya mencoba mengurangi 1 dari 0, saya mendapatkan ...

 Test 2/2 -...integer overflow 

mem adalah array byte yang tidak ditandatangani, dan mengurangi 1 dari 0 menyebabkan overflow. Sekali lagi, Zig membuat saya menyatakan apa yang saya inginkan secara eksplisit. Dalam hal ini, saya tidak perlu khawatir meluap, pada kenyataannya, saya ingin itu terjadi, karena kita berhadapan dengan aritmatika modular , sesuai dengan spesifikasi brainfuck . Ini berarti bahwa pengurangan sel dengan angka 0 akan memberi saya 255, dan kenaikan 255 akan memberi saya 0.

Zig memiliki beberapa operasi aritmatika tambahan yang menawarkan semantik “pembungkus” yang dijamin .

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

Ini menyelesaikan seluruh masalah overflow dan melakukan apa yang saya harapkan.

Untuk menguji <and>, saya menavigasi melalui array kecil dan memeriksa nilai sel yang bertambah:

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

dan ...

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

Dalam kasus terakhir, saya bisa langsung membandingkan hasilnya dengan array statis menggunakan ...

 const mem = std.mem; 

Ingat bahwa saya sudah mengimpor std. Dalam contoh di bawah ini, saya menggunakan mem.eql di namespace ini:

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

... dan ingat, string literal, ini hanya array u8 dalam zig, dan saya dapat menempatkan liter heksadesimal di dalamnya, mis. Kode berikut akan bekerja dengan cara yang sama!

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

Tambahkan "."! Ini hanya mencetak sebagai karakter nilai byte dalam sel yang ditunjuk oleh pointer. Saya menggunakan memperingatkan sekarang, tetapi nanti saya akan menggantinya dengan stdout. Ini mudah dilakukan secara konseptual, tetapi agak membingungkan dalam implementasinya. Saya akan melakukannya nanti!

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

Siklus
[dan] - keajaiban dimulai di sini ....

[- jika nilai sel saat ini nol, lewati langkah-langkah ke braket penutup tanpa mengeksekusi kode.
] - jika nilai sel saat ini tidak nol, kembali ke braket pembuka dan jalankan kode lagi.

Kali ini saya akan mulai dengan tes, saya akan mengujinya bersama (jelas, tidak masuk akal untuk mengujinya secara terpisah). Kasing uji pertama - sel penyimpanan [2] harus kosong, meskipun loop harus menambahnya jika dimulai:

 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); } 

dan saya akan membuat blank untuk statement switch:

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

Apa yang harus dilakukan sekarang? Anda dapat menggunakan pendekatan naif. Saya hanya menambah pointer src sampai saya menemukannya]. Tapi saya tidak bisa menggunakan for for loop in zig untuk ini, itu dibuat hanya untuk iterasi melalui koleksi, tanpa kehilangan elemen mereka. Konstruk yang sesuai di sini adalah sementara:

adalah:

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

menjadi ...

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

Sekarang saya dapat menetapkan kembali pointer srcptr di tengah blok, saya akan melakukan ini:

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

Ini memenuhi tes "[] melewatkan eksekusi kode dan keluar"
Ini memuaskan tes “[] melompati eksekusi dan keluar”, meskipun tidak sepenuhnya dapat diandalkan, seperti yang akan kita lihat.

Bagaimana dengan menutup kurung? Saya percaya ini dapat ditulis hanya dengan analogi:

 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; }, 

Anda dapat melihat apa yang terjadi ... Solusi naif dengan dua tanda kurung memiliki cacat fatal dan benar-benar terputus pada loop bersarang. Pertimbangkan yang berikut ini:

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

Hasilnya harus {2, 0}, tetapi braket terbuka pertama dengan bodohnya bergerak ke braket penutup pertama, dan semuanya menjadi berantakan. Anda harus melompat ke braket penutup berikutnya di tingkat yang sama dari sarang. Sangat mudah untuk menambahkan penghitung kedalaman dan melacaknya saat Anda bergerak maju sepanjang garis. Kami melakukannya di kedua arah:

 '[' => 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 => {} } } }, 

dan tes terkait: perhatikan bahwa src di kedua tes termasuk loop dalam.

 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); } 

Secara terpisah, perhatikan [-] - idiom dari brainfuck, yang berarti "nol sel ini." Anda dapat melihat bahwa tidak masalah apa nilai sel di awal, itu akan dikurangi hingga mencapai 0, dan kemudian eksekusi akan berlanjut.

Jalan sial


Saya tidak mengandalkan kemungkinan bahwa program pada bf akan rusak. Apa yang terjadi jika saya mengirimkan program input yang salah ke juru bahasa saya? Misalnya, cukup [tanpa braket penutup, atau <, yang segera melampaui array memori? (Saya dapat membungkus penunjuk memori, tetapi lebih baik untuk menganggap ini sebagai kesalahan).

Saya akan melihat sedikit ke depan dan menjelaskan semua perbedaan dalam kode. Saya akan meletakkan fungsi juru bahasa ke dalam file terpisah dan juga menempatkan fungsi seekBack dan seekForward ke dalam fungsi kecil saya sendiri.

 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; } } 

Ini membuat peralihan jauh lebih mudah dibaca, menurut saya, seekForward dan seekBack bekerja dan terlihat sangat mirip, dan saya tergoda untuk mengubah mereka menjadi sesuatu yang lebih pintar dan lebih kompak, tetapi pada akhirnya mereka melakukan hal yang berbeda dan menangani kesalahan. juga dengan cara yang berbeda. Lebih mudah untuk menyalin dan menyesuaikan, sehingga akan lebih jelas. Saya juga akan menyesuaikan seekForward nanti, di beberapa titik, mungkin di posting selanjutnya.

Saya menambahkan beberapa hal penting! Perhatikan bahwa ketiga fungsi sekarang mengembalikan tipe! .. Ini adalah sintaks baru untuk tipe% T (kesalahan gabungan). Ini berarti bahwa fungsi tersebut dapat mengembalikan beberapa jenis tertentu atau kesalahan. Ketika saya mencoba memanggil fungsi seperti itu, saya harus menggunakan coba sebelum memanggil fungsi, yang melempar kesalahan ke tumpukan panggilan jika kesalahan terjadi, atau menggunakan catch:

 const x = functionCall() catch {} 

Di mana saya menangani kesalahan di blok tangkap. Seperti yang tertulis, catch dapat menelan kesalahan. Ini adalah praktik yang buruk, tetapi di sini Zig membuat kami melakukannya secara eksplisit. Jika saya menemukan kesalahan di blok kosong, saya menyatakan bahwa saya tidak berpikir bahwa kesalahan dapat terjadi, atau saya tidak perlu menanganinya. Dalam praktiknya, ini bisa seperti TODO, dan sebenarnya sangat mudah membuatnya juga eksplisit!

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

Ingatlah bahwa kasus seperti itu tidak akan pernah terjadi dalam kode produksi. Saya memberi tahu kompilator bahwa saya tahu apa yang saya lakukan. Jika kesalahan dapat terjadi, saya harus menambahkan penanganan kesalahan.

Jadi kesalahan apa yang harus saya kembalikan dari seekBack atau seekForward?

Di seekBack:

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

Saya mengganti pointer decrement untuk menggunakan fungsi sub std lib, yang melempar kesalahan overflow jika terjadi overflow. Saya ingin menangkap kesalahan ini dan mengembalikan kesalahan OutOfBounds, yang saya buat di sini hanya dengan menggunakannya.

Kesalahan Zig pada dasarnya adalah larik kode kesalahan yang dihasilkan oleh kompiler saat Anda menggunakan kesalahan. Mereka dijamin unik dan dapat digunakan sebagai nilai dalam blok switch.

Saya ingin menggunakan OutOfBounds di sini karena, secara semantik, jika penunjuk memori menjadi kurang dari nol, saya meminta runtime untuk melampaui ruang memori yang saya alokasikan.

sama dalam fungsi seekForward:

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

Dalam hal ini, jika pointer lebih besar dari src.len, saya menangkap kesalahan di sini dan mengembalikan kesalahan yang sama.

saat menelepon:

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

Saya mencoba memanggil fungsi-fungsi ini. Jika mereka dipanggil dengan sukses, mereka dieksekusi dengan benar, dan coba kembalikan srcptr. Jika tidak berhasil, coba akhiri fungsi dan kembalikan kesalahan ke tempat panggilan ke seluruh fungsi bf.

Panggilannya mungkin dari utama!

 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 {}; } 

Saya menelan kesalahan ini di sini, dan itu tidak boleh dilakukan, tetapi kami akan mencatat poin penting tentang betapa mudahnya zig dapat melewatkan kesalahan pada tumpukan panggilan. Ini bukan tanggung jawab fungsi panggilan untuk memeriksa setiap kasus kesalahan, tetapi kompiler memaksa panggilan setiap fungsi yang dapat gagal dengan percobaan. Ini harus selalu dilakukan, bahkan jika kesalahan diabaikan!

Sintaks coba / tangkapan baru menghilangkan banyak mantra seperti %% dan% yang sangat tidak disukai orang.

Sekarang saya telah mengimplementasikan 7 dari 8 karakter brainfuck, dan ini cukup untuk menjalankan program yang "bermakna".

Program yang bermakna


Inilah programnya:

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

Ayo lari ...

 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, 

Satu ingatan kembali kepada saya setiap kali saya memikirkan seri Fibonacci ... Saya mengetahuinya dari program PBS (Public Broadcasting Service, layanan penyiaran televisi non-komersial Amerika) di tahun 80-an, dan saya selalu ingat itu. Saya pikir itu akan dilupakan, tetapi Youtube adalah hal yang hebat .

Bagaimana saya bisa meningkatkan ini?


Saya sudah mengisyaratkan beberapa TODO. Seharusnya saya tidak menggunakan stderr untuk output. Saya ingin menggunakan stdout.

Setiap kali saya membuka juru bahasa, saya membuka aliran di stdout dan mencetak ke dalamnya:

 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, ... 

Apa yang sedang terjadi di sini?Saya memanggil io.getStdOut (), yang dapat menghasilkan kesalahan (dan sekali lagi, saya secara eksplisit menelan kemungkinan kesalahan dengan catch unreachable - jika fungsi ini mengembalikan kesalahan, program akan macet!). Saya menginisialisasi aliran, mengambil pointer ke sana, dan menginisialisasi sebagai aliran keluaran yang saya dapat menulis dengan memanggil print. print menerima string yang diformat, seperti halnya peringatan, maka penggantian langsung. cetak juga dapat menghasilkan kesalahan, dan saya menelan kesalahan ini juga.

Dalam program yang ditulis dengan benar, saya harus memperhitungkan potensi kesalahan membuka stdout, serta kemungkinan kesalahan saat mencoba menulis ke stdout. Zig membuatnya sangat mudah untuk mengabaikan kesalahan-kesalahan ini selama Anda tahu bahwa Anda mengabaikannya.

Apa yang terjadi jika saya memutuskan bahwa saya ingin mengubah prototipe saya menjadi rilis? Akankah saya duduk dengan secangkir kopi dan melakukan pekerjaan yang tidak tahu berterima kasih dalam menangani kesalahan, mengandalkan pengalaman dan pengetahuan selama puluhan tahun untuk membuat daftar setiap kasus kesalahan yang mungkin terjadi, dan bagaimana saya bisa mengatasinya? Tetapi bagaimana jika saya tidak memiliki pengalaman dan pengetahuan selama puluhan tahun? Tidak apa-apa, Zig akan melakukannya!

Saya ingin menunjukkan hal yang kuat, output kesalahan!

 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; } 


Saya tahu bahwa bf dapat menghasilkan kesalahan karena ia kembali! Batal. Saya menelan kesalahan ini di sisi panggilan, di fungsi utama. Ketika saya siap menerima nasib saya dan melakukan hal yang benar, saya dapat menangkap kemungkinan kesalahan seperti ini:

 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) { }; } 

Kompiler sekarang adalah temanku!

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

Kesalahan ini seharusnya sudah tidak asing lagi bagi Anda, karena itu muncul dari fungsi bf dan tambahan! Tapi mari kita bayangkan bahwa saya melihat kesalahan yang dihasilkan oleh stdout yang saya telan dalam bf. Alih-alih menelan mereka, saya harus mendorong mereka ke atas menggunakan percobaan. Ingatlah bahwa menggunakan panggilan ke fungsi yang menghasilkan kesalahan tanpa tangkapan, kami menggunakan coba, yang mengakhiri fungsi ketika terjadi kesalahan, menyediakan fungsi panggilan dengan penanganan potensi kesalahan.

Jadi, alih-alih:

 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, ... 

Kami lakukan:

 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]), ... 

Kami mengkompilasi:

 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) { }; } 

dan dapatkan daftar semua kemungkinan kesalahan yang bisa saya dapatkan dengan memanggil fungsi!

 /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 memberi saya kesempatan untuk menangani kesalahan ini dengan hati-hati jika saya mau atau bisa melakukannya! Saya membuat switch berdasarkan nilai kesalahan, menangani kasus jika saya mau, dan melewatkan jika saya ingin melewatkannya.

 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") }; } 

Ini masih belum benar penanganan kesalahan, tetapi saya hanya ingin menunjukkan seberapa pintar Zig dengan melaporkan semua jenis kasus kesalahan ke fungsi panggilan! Dan ketika kesalahan terjadi, Anda mendapatkan jejak kesalahan alih-alih jejak tumpukan! Hal yang keren!

Todo


Ada banyak peningkatan berbeda yang dapat Anda lakukan dengan penerjemah! Anda harus benar-benar menangani semua kesalahan dengan benar, jelas, dan Anda perlu mengimplementasikan operator ",", yang dalam brainfuck bertindak sebagai fungsi getc, memungkinkan Anda memasukkan data ke dalam program saat dijalankan. Anda juga harus memungkinkan untuk membaca file sumber ke buffer dan menafsirkannya, daripada menggunakan kode sumber bf hardcoded. Ada juga beberapa perbaikan yang tidak sepenuhnya diperlukan, tetapi dapat menggambarkan beberapa fitur Zig. Alih-alih membuang semuanya di akhir posting, saya akan membaginya menjadi beberapa bagian dan mempublikasikannya di posting mendatang, yang akan lebih kecil dan lebih mudah dicerna.

Kesimpulan


Saya harap proyek miniatur yang setengah jadi ini memberi Anda wawasan tentang seperti apa kode Zig dan apa yang bisa digunakan untuk itu. Zig bukan pisau Swiss, itu bukan alat yang sempurna untuk semuanya, itu berfokus pada hal-hal tertentu, menjadi bahasa sistem pragmatis yang dapat digunakan bersama-sama atau bukan C dan C ++. Ini membuat saya dengan hati-hati mendekati penggunaan memori, manajemen memori, dan penanganan kesalahan. Dalam lingkungan dengan sumber daya terbatas, ini adalah fitur yang berguna, bukan bug. Zig bersifat deterministik, tidak memiliki ambiguitas, dan mencoba memfasilitasi penulisan kode yang andal dalam lingkungan yang secara tradisional sulit dilakukan.

Saya menjelaskan hanya sebagian kecil dari sintaks dan fitur Zig, ada banyak perubahan menarik yang datang ke bahasa dalam versi 0.2.0 dan lebih tinggi! Semua kode yang saya tulis dikompilasi dalam mode debug, yang optimal untuk pemeriksaan keamanan dan mengurangi waktu kompilasi untuk membuat iterasi lebih cepat! Ada mode --release-fast dan --release-safe, dan akan ada lebih banyak di masa depan . Anda dapat membaca lebih lanjut tentang perbedaan mereka dan penjelasan tentang mode ini di sini .

Saya selalu kagum dengan kecepatan dan arah pengembangan Zig. Banyak yang masih bergerak, dan akan tetap demikian hingga rilis versi 1.0.0, dan jika Anda memutuskan untuk mencoba Zig, ingat saja, ada banyak ide bagus, dan saya berharap dapat mengimplementasikannya!

Cobalah dan bergabunglah dengan #zig di freenode kapan saja jika Anda memiliki pertanyaan.

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


All Articles