来自翻译者:这篇文章于2018年3月15日发布在作者的博客上。 随着语言的发展,目前其语法可能有所不同。 所描述的所有内容均与Zig 0.2.0有关,该语言的当前版本为Zig 0.3.0。
我联系了帖子的作者,他在Zig 0.3.0上提供了到项目源的最新版本的资源库链接。
你好 让我们写一个Brainfuck解释器! “为什么?” “您可能会问,但在这里找不到答案。”
我将在
Zig上执行此操作。

Zig是...
...一种新的编程语言。 它仍处于测试阶段,并且正在迅速发展。 如果您之前看过Zig代码,那么本篇文章中的代码似乎对您而言就有些不同了。 他真的不一样! Zig 0.2.0刚刚发布,与几周前发布的
LLVM 6一致,它包括许多语法更改和常规语言改进。 通常,许多“咒语”已被关键字替换。 有关所有更改的更详细说明,请参见
此处 !
对于熟悉C和C ++等某些编译和类型化语言(在某些情况下还包括Rust)的人来说,Zig的设计
具有可读性和相对直观性。
该代码已使用Zig 0.2.0进行编译和测试,如果您在OSX上,则可
通过各种渠道 (包括自制软件)使用Zig 0.2.0:brew install zig。
让我们开始吧
要了解Brainfuck的工作原理,请参见
此处 。 那里几乎没有什么可学的,但这是一种
图灵完备的语言,这意味着您可以在上面写
任何东西 。
我在
此处发布了代码,以防您想查看最终产品或早期提交。
Zig是一种编译语言。 编译程序时,生成的二进制文件(如果正在编译可执行二进制文件,而不是库)应具有标记入口点的主函数。
所以...
// main.zig fn main() void { }
...然后开始...
$ zig build-exe main.zig
...发出...
/zig/std/special/bootstrap.zig:70:33: error: 'main' is private /zigfuck/main.zig:2:1: note: declared here
main必须声明为public以便在模块外部可见...
// main.zig pub fn main() void { }
让Brainfuck程序使用30,000字节的数组作为内存,我将制作这样的数组。
// main.zig pub fn main() void { const mem: [30000]u8; }
我可以声明一个常量(const)或一个变量(var)。 在这里,我将mem声明为30,000个无符号(u)字节(8位)的数组。
这不会编译。
/main.zig:3:5: error: variables must be initialized
等效的C程序将正常编译:我可以在不初始化的情况下声明变量,但是Zig迫使我在声明变量时立即做出决定。 我可能不在乎会写什么,但是我必须明确指出。 我将通过使用未定义的值(未定义)初始化变量来完成此操作。
// main.zig pub fn main() void { const mem: [30000]u8 = undefined; }
用未定义的值初始化变量并不能保证该变量在内存中的值。 这与C中的未初始化变量声明相同,除了需要明确指出。
但也许我不在乎如何初始化此内存。 也许我想保证在其中写入零或一些任意值。 在这种情况下,我还应该明确声明:
// main.zig pub fn main() void { const mem = []u8{0} ** 30000; }
看起来很奇怪,但是**是用于扩展数组的运算符。 我声明一个0字节的数组,然后将其扩展为30,000,并获得30,000个零字节的最终初始化值。 该操作
在编译时发生一次。 comptime是Zig的出色创意之一,我将在以下几篇文章中再次谈到。
现在让我们在Brainfuck上编写一个程序,该程序除了将第一个内存插槽增加五倍之外什么也不做!
pub fn main() void { const mem = []u8{0} ** 30000; const src = "+++++"; }
在Zig中,字符串是字节数组。 我不应该将src声明为字节数组,因为编译器暗含了这一点。 这是可选的,但是如果您愿意,则可以:
const src: [5]u8 = "+++++";
这样可以编译良好。 但是,这:
const src: [6]u8= "+++++";
不会。
main.zig:5:22: error: expected type '[6]u8', found '[5]u8'
还有一点需要注意:由于字符串只是数组,所以它们不以零结尾。 但是,您可以声明一个以空值终止的字符串C。作为文字,它看起来像这样:
c"Hello I am a null terminated string";
为了共同的利益...
我想对字符串中的每个字符进行处理。 我做得到! 在main.zig的开头,我从标准库中导入了一些函数:
const warn = @import("std").debug.warn;
就像几乎所有以@符号开头的内容一样,
import都是
内置的编译器函数 。 这些功能始终在全球范围内可用。 此处的导入与javascript相似-您可以通过深入名称空间并从中提取任何公开可用的函数或变量来导入任何内容。 在上面的示例中,我直接导入了警告函数,并突然将其分配给警告常量。 现在可以打电话给她了。 这是一种常见的模式:我们直接从std名称空间导入,然后调用std.debug.warn()或将其分配给warn变量。 看起来像这样:
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); } }
在调试,初始开发和测试过程中,我只想在屏幕上打印一些内容。 Zig
容易出错 ,stdout也容易出错。 我现在不想这样做,我可以使用从标准库导入的warn直接打印到stderr。
警告使用格式化的字符串,例如C语言中的printf! 上面的代码将打印:
4343434343
43是ASCII字符代码+。 我也可以写:
warn("{c}", c);
并获得:
+++++
因此,我们初始化了内存空间,并编写了程序。 现在我们正在意识到语言本身。 我将从+开始,并用switch替换for循环的主体:
for (src) |c| { switch(c) { '+' => mem[0] += 1 } }
我得到两个错误:
/main.zig:10:7: error: switch must handle all possibilities switch(c) { ^ /main.zig:11:25: error: cannot assign to constant '+' => mem[0] += 1 ^
当然,我不能将新值赋给一个常量变量! 记忆需要成为一个变量...
var mem = []u8{0} ** 30000;
与其他错误一样,即使不需要执行任何操作,我的
switch构造也应该知道如果字符不是+怎么办。 就我而言,这正是我想要的。 我用一个空块填充这种情况:
for (src) |c| { switch(c) { '+' => mem[0] += 1, else => {} } }
现在,我可以编译程序了。 最后致电警告并运行:
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]); }
如我所料,我在
stderr中得到了数字5。
让我们继续...
同样,我们也提供支持。
switch(c) { '+' => mem[0] += 1, '-' => mem[0] -= 1, else => {} }
要使用>和<,您需要使用一个附加变量,该变量在我分配给用户Brainfuck程序的内存中用作“指针”。
var memptr: u16 = 0;
由于无符号的16位最大为65535,因此索引30,000字节的地址空间绰绰有余。
实际上,对于我们来说15位就足够了,这使我们可以寻址32767个字节。 Zig允许使用不同宽度的类型,但尚未允许u15。
您实际上可以通过以下方式执行u15:
const u15 = @IntType(false, 15):
建议任何[iu] \ d +类型都可以作为整数类型有效。
现在,我可以不使用mem [0],而可以使用此变量。
'+' => mem[memptr] += 1, '-' => mem[memptr] -= 1,
<and>只需递增和递减此指针。
'>' => memptr += 1, '<' => memptr -= 1,
太好了 我们现在可以编写一个真实的程序!
检查1,2,3
Zig具有内置的测试引擎。 在任何文件中的任何地方,我都可以编写一个测试块:
test "Name of Test" { // test code }
并从命令行运行测试:zig test $ FILENAME。 其余测试块与常规代码相同。
让我们来看一下:
// test.zig test "testing tests" {} zig test test.zig Test 1/1 testing tests...OK
当然,空测试是没有用的。 我可以使用assert来确认测试的执行。
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
测试失败了。 使用以下命令来重现该错误:
./zig-cache/test
罂粟上的堆栈跟踪仍在开发中。为了有效地进行测试,我需要将其分解。 让我们从这个开始:
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); }
它似乎应该起作用,对吗?
但是...
/main.zig:1:29: error: type '[30000]u8' is not copyable; cannot pass by value
这在https://github.com/zig-lang/zig/issues/733中进行了描述。
齐格对此很严格。 复杂类型以及所有可以调整大小的对象都不能按值传递。 这使堆栈分配可预测且合乎逻辑,并避免了不必要的复制。 如果要在程序中使用按值传递的语义,则可以使用分配策略自己实现它,但是在通常情况下,语言本身不支持此操作。
解决此限制的自然方法是传递指针而不是值(按引用传递)。 Zig使用不同的策略,切片。 切片是一个指针,其长度与其相连,并带有检查是否落入边界的指示。 函数签名中的语法如下所示:
fn bf(src: []const u8, mem: []u8) void { ... }
并在调用该函数时如下所示:
bf(src, mem[0..mem.len]);
请注意,我只是通过引用数组的长度来定义上限。 对于这种情况,有一种缩写形式:
bf(src, mem[0..]);
现在,我可以开始编写直接测试bf()函数的测试。 我现在将测试功能添加到文件的末尾...
test "+" { var mem = []u8{0}; const src = "+++"; bf(src, mem[0..]); assert(mem[0] == 3); }
我从一个字节中取出内存数组,然后检查会发生什么(该字节增加3次)。 有效!
Test 1/1 +...OK
以相同的方式检查“-”:
test "-" { var mem = []u8{0}; const src = "---"; bf(src, mem[0..]); assert(mem[0] == 253); }
不起作用! 当我尝试从0中减去1时,我得到...
Test 2/2 -...integer overflow
mem是无符号字节的数组,从0减去1会导致溢出。 同样,Zig让我明确声明自己想要的东西。 在这种情况下,我不必担心溢出,实际上,我希望它发生,因为我们正在按照
Brainfuck的
规范处理
模块化算术 。 这意味着递减一个数字为0的单元格将得到255,而递增255将给我0。
Zig具有几种辅助算术运算,这些运算提供了
保证“包装”的
语义 。
'+' => mem[memptr] +%= 1, '-' => mem[memptr] -%= 1,
这解决了整个溢出问题,并达到了我的预期。
为了测试<and>,我浏览了一个小数组并检查了递增单元格的值:
test ">" { var mem = []u8{0} ** 5; const src = ">>>+++"; bf(src, mem[0..]); assert(mem[3] == 3); }
还有...
test "<" { var mem = []u8{0} ** 5; const src = ">>>+++<++<+"; bf(src, mem[0..]); assert(mem[3] == 3); assert(mem[2] == 2); assert(mem[1] == 1); }
在后一种情况下,我可以使用...直接将结果与静态数组进行比较。
const mem = std.mem;
回想一下我已经导入了std。 在下面的示例中,我在此命名空间中使用mem.eql:
test "<" { var storage = []u8{0} ** 5; const src = ">>>+++<++<+"; bf(src, storage[0..]); assert(mem.eql(u8, storage, []u8{ 0, 1, 2, 3, 0 })); }
...记住字符串字面量,它们只是zig中的u8数组,我可以在其中放入十六进制字面量,即 以下代码将以相同的方式工作!
assert(mem.eql(u8, storage, "\x00\x01\x02\x03\x00"));
添加“。”! 它只是将指针所指向的单元格中的字节值打印为字符。 我现在使用警告,但是稍后我将其替换为stdout。 从概念上讲,这很容易做到,但在实现上却有些困惑。 我稍后再做!
'.' => warn("{c}", storage[memptr]),
周期数
[和]-魔术从这里开始。
[-如果当前单元格的值为零,则不执行代码就跳至右括号。
]-如果当前单元格的值不为零,请返回左括号并再次执行代码。
这次,我将从测试开始,将它们一起测试(显然,单独测试它们没有意义)。 第一个测试用例-存储[2]单元应该为空,尽管循环在开始时应将其递增:
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); }
我将为switch语句创建空白:
'[' => if (storage[memptr] == 0) { }, ']' => if (storage[memptr] == 0) { },
现在该怎么办? 您可以使用幼稚的方法。 我只是增加src指针,直到找到它为止。 但是我不能在zig中使用for循环,它是为遍历集合而创建的,而不会丢失它们的元素。 这里是一个合适的构造:
原为:
var memptr: u16 = 0; for (src) |c| { switch(c) { ... } }
成为...
var memptr: u16 = 0; var srcptr: u16 = 0; while (srcptr < src.len) { switch(src[srcptr]) { ... } srcptr += 1; }
现在,我可以在块中间重新分配srcptr指针,我将这样做:
'[' => if (storage[memptr] == 0) { while (src[srcptr] != ']') srcptr += 1; },
这满足测试“ []跳过代码执行并退出”
正如我们将看到的那样,它满足测试“ []跳过执行并退出”,尽管并不完全可靠。
右括号呢? 我相信可以简单地用类推来写:
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; },
您可以看到发生了什么...两个括号的幼稚解决方案具有致命缺陷,并且在嵌套循环中完全中断。 考虑以下内容:
++>[>++[-]++<-]
结果应该是{2,0},但是第一个左括号简单地愚蠢地移到了第一个右括号,并且一切都变得混乱了。 您需要跳到相同嵌套级别的下一个右括号。 沿直线前进时,添加深度计数器并对其进行跟踪很容易。 我们在两个方向上都这样做:
'[' => 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 => {} } } },
和相关测试:请注意,两个测试中的src都包含一个内部循环。
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); }
另外,请注意[-]-Brainfuck的成语,意思是“将此单元归零”。 您会看到,单元格开始时具有什么值都没有关系,它将递减直到达到0,然后继续执行。
倒霉的路
我没有指望bf上的程序被破坏的可能性。 如果我向口译员提交了错误的输入程序,该怎么办? 例如,简单地[没有右括号或<,它立即超出了内存阵列? (我可以包装内存指针,但是最好将此视为错误)。
我将向前看一些,并解释代码中的所有差异。 我将把bf解释器功能放到一个单独的文件中,还将seekBack和seekForward功能放到我自己的小功能中。
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; } }
在我看来,这使开关更易于阅读,seekForward和seekBack的工作和外观非常相似,我很想将它们重构为更智能,更紧凑的东西,但最终它们会做不同的事情并处理错误也以不同的方式。 易于复制和调整,因此更加清晰。 我还将稍后在某个时候(可能在后续帖子中)调整seekForward。
我添加了一些重要的东西! 请注意,这三个函数现在都返回一个类型!..这是用于%T(错误并集)类型的新语法。 这意味着该函数可以返回某些特定类型或错误。 当我尝试调用此类函数时,必须在调用该函数之前使用try,如果发生错误,则该错误会将错误抛出到调用堆栈中,或者使用catch:
const x = functionCall() catch {}
我在catch块中处理错误的地方。 如所写,catch可以吞下任何错误。 这是不好的做法,但是Zig在这里让我们明确地做到了。 如果我在一个空块中捕获到错误,则说明我不认为会发生错误,或者不需要处理它。 实际上,它可能类似于TODO,实际上也很容易使其明确!
const x = functionCall() catch { @panic("TODO") }
回想一下,这种情况永远不会在生产代码中发生。 我通知编译器我知道自己在做什么。 如果可能发生错误,我将不得不添加错误处理。
那么我应该从seekBack或seekForward返回什么错误?
在seekBack中:
ptr = sub(u16, ptr, 1) catch return error.OutOfBounds;
我替换了减量指针以使用std lib的子函数,如果发生溢出,它将引发溢出错误。 我想捕获此错误并返回OutOfBounds错误,而我只是在使用它创建的。
错误Zig基本上是错误代码的数组,当您使用error时,编译器会生成这些错误代码。 它们保证是唯一的,并且可以用作开关块中的值。
我想在这里使用OutOfBounds,因为从语义上讲,如果内存指针小于零,我会要求运行时超出我分配的内存空间。
在seekForward函数中类似:
if (ptr >= src.len) return error.OutOfBounds;
在这种情况下,如果指针大于src.len,我会在这里捕获错误并返回相同的错误。
致电时:
'[' => if (storage[memptr] == 0) srcptr = try seekForward(src, srcptr), ']' => if (storage[memptr] != 0) srcptr = try seekBack(src, srcptr),
我尝试调用这些函数。 如果成功调用了它们,则它们将正确执行,并尝试返回srcptr。 如果它们不成功,请尝试终止该函数,并将错误返回到整个函数bf的调用位置。
呼叫可能来自主要!
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 {}; }
我在这里吞并了这个错误,不应该这样做,但是我们要注意一个重要的问题,即zig可以很容易地将错误传递到调用堆栈中。 检查每种错误情况都不是调用函数的责任,但是编译器会强制尝试尝试失败的每个函数。 即使忽略错误,也必须始终这样做!
新的try / catch语法消除了人们非常讨厌的许多咒语,例如%%和%。
现在,我已经实现了8个脑残角色中的7个,这足以运行一个“有意义的”程序。
一个有意义的程序
这是程序:
// , const fib = "++++++++++++++++++++++++++++++++++++++++++++>++++++++++++++++++++++++++++++++>++++++++++++++++>>+<<[>>>>++++++++++<<[->+>-[>+>>]>[+[-<+>]>+>>]<<<<<<]>[<+>-]>[-]>>>++++++++++<[->-[>+>>]>[+[-<+>]>+>>]<<<<<]>[-]>>[++++++++++++++++++++++++++++++++++++++++++++++++.[-]]<[++++++++++++++++++++++++++++++++++++++++++++++++.[-]]<<<++++++++++++++++++++++++++++++++++++++++++++++++.[-]<<<<<<<.>.>>[>>+<<-]>[>+<<+>-]>[<+>-]<<<-]<<++...";
让我们开始...
pub fn main() void { storage = []u8{0} ** 30000; bf(fib, storage[0..]) catch {}; }
瞧!
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 121, 98, 219,
每当我想到斐波那契(Fibonacci)系列电影时,我都会回想起……我是从80年代的PBS(公共广播服务,美国的非商业电视广播服务)计划中发现的,我一直都记得。 我以为会被遗忘,但是Youtube是一件很棒的事 。
我该如何改善?
我已经暗示了一些待办事项。 我不应该使用stderr进行输出。 我想使用标准输出。
每次打开解释器时,我都会在stdout中打开流并打印到其中:
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, ...
这是怎么回事
我调用io.getStdOut(),它会生成错误(同样,我明确地吞下了一个可能的错误,并且catch无法到达-如果此函数返回错误,程序将崩溃!)。我初始化流,获取它的指针,然后将其初始化为可通过调用print写入的输出流。就像警告一样,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会因为返回而产生错误!我在主函数的调用端吞下了此错误。当我准备好接受命运并做正确的事时,我会发现以下可能的错误: 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和辅助功能引发了该错误,因此您应该熟悉该错误!但是,让我们想象一下,我看到了我在bf中吞下的stdout生成的错误。与其吞下它们,不如尝试使用它们将它们推上链。回想一下,使用对产生错误而不捕获的函数的调用,我们使用了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让我有机会认真地处理这些错误,如果我愿意或可以做到!我根据错误值进行切换,如果需要,可以处理大小写,如果要跳过它们,则可以跳过。 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的智能程度!当发生错误时,您将获得错误跟踪而不是堆栈跟踪!很酷的东西!待办
, ! , , ",", brainfuck- getc, . , bf. , , Zig. , , , .
结论
我希望这个完成了一半的微型项目可以使您对Zig代码的外观以及它的用途有所了解。 Zig不是一把瑞士刀,它也不是所有工具的完美工具,它专注于某些事情,是一种实用的系统语言,可以一起使用,也可以代替C和C ++使用。这使我仔细地处理了内存使用,内存管理和错误处理。在资源有限的环境中,这是一个有用的功能,而不是错误。 Zig是确定性的,没有歧义,并且试图在传统上难以做到的环境中促进可靠代码的编写。我仅描述了Zig语法和功能的一小部分,在0.2.0版及更高版本中对该语言进行了许多有趣的更改!我编写的所有代码都是在调试模式下编译的,这对于安全检查和减少编译时间以使迭代速度更快而言是最佳选择!有--release-fast和--release-safe模式,将来还会有更多。您可以在此处详细了解它们的区别以及对这些模式的解释。我一直对Zig开发的速度和方向感到惊讶。仍然有很多工作要做,直到1.0.0版发行为止。如果您决定尝试Zig,请记住,这里有很多好主意,我期待着实现它们!如有疑问,请随时尝试并在freenode中加入#zig。