mirror of
https://github.com/ziglang/zig.git
synced 2026-02-20 00:08:56 +00:00
parent
cdf1e366f9
commit
f0697c28f8
@ -590,6 +590,7 @@ test "initialization" {
|
||||
x = 1;
|
||||
}
|
||||
{#code_end#}
|
||||
{#header_open|undefined#}
|
||||
<p>Use <code>undefined</code> to leave variables uninitialized:</p>
|
||||
{#code_begin|test#}
|
||||
const assert = @import("std").debug.assert;
|
||||
@ -602,6 +603,7 @@ test "init with undefined" {
|
||||
{#code_end#}
|
||||
{#header_close#}
|
||||
{#header_close#}
|
||||
{#header_close#}
|
||||
{#header_open|Integers#}
|
||||
{#header_open|Integer Literals#}
|
||||
{#code_begin|syntax#}
|
||||
@ -2999,6 +3001,7 @@ test "parse u64" {
|
||||
<li>You know with complete certainty it will not return an error, so want to unconditionally unwrap it.</li>
|
||||
<li>You want to take a different action for each possible error.</li>
|
||||
</ul>
|
||||
{#header_open|catch#}
|
||||
<p>If you want to provide a default value, you can use the <code>catch</code> binary operator:</p>
|
||||
{#code_begin|syntax#}
|
||||
fn doAThing(str: []u8) void {
|
||||
@ -3011,6 +3014,8 @@ fn doAThing(str: []u8) void {
|
||||
a default value of 13. The type of the right hand side of the binary <code>catch</code> operator must
|
||||
match the unwrapped error union type, or be of type <code>noreturn</code>.
|
||||
</p>
|
||||
{#header_close#}
|
||||
{#header_open|try#}
|
||||
<p>Let's say you wanted to return the error if you got one, otherwise continue with the
|
||||
function logic:</p>
|
||||
{#code_begin|syntax#}
|
||||
@ -3033,6 +3038,7 @@ fn doAThing(str: []u8) !void {
|
||||
from the current function with the same error. Otherwise, the expression results in
|
||||
the unwrapped value.
|
||||
</p>
|
||||
{#header_close#}
|
||||
<p>
|
||||
Maybe you know with complete certainty that an expression will never be an error.
|
||||
In this case you can do this:
|
||||
@ -3047,7 +3053,7 @@ fn doAThing(str: []u8) !void {
|
||||
</p>
|
||||
<p>
|
||||
Finally, you may want to take a different action for every situation. For that, we combine
|
||||
the <code>if</code> and <code>switch</code> expression:
|
||||
the {#link|if#} and {#link|switch#} expression:
|
||||
</p>
|
||||
{#code_begin|syntax#}
|
||||
fn doAThing(str: []u8) void {
|
||||
@ -3062,9 +3068,10 @@ fn doAThing(str: []u8) void {
|
||||
}
|
||||
}
|
||||
{#code_end#}
|
||||
{#header_open|errdefer#}
|
||||
<p>
|
||||
The other component to error handling is defer statements.
|
||||
In addition to an unconditional <code>defer</code>, Zig has <code>errdefer</code>,
|
||||
In addition to an unconditional {#link|defer#}, Zig has <code>errdefer</code>,
|
||||
which evaluates the deferred expression on block exit path if and only if
|
||||
the function returned with an error from the block.
|
||||
</p>
|
||||
@ -3095,6 +3102,7 @@ fn createFoo(param: i32) !Foo {
|
||||
the verbosity and cognitive overhead of trying to make sure every exit path
|
||||
is covered. The deallocation code is always directly following the allocation code.
|
||||
</p>
|
||||
{#header_close#}
|
||||
<p>
|
||||
A couple of other tidbits about error handling:
|
||||
</p>
|
||||
@ -3223,7 +3231,174 @@ test "inferred error set" {
|
||||
{#header_close#}
|
||||
{#header_close#}
|
||||
{#header_open|Error Return Traces#}
|
||||
<p>TODO</p>
|
||||
<p>
|
||||
Error Return Traces show all the points in the code that an error was returned to the calling function. This makes it practical to use {#link|try#} everywhere and then still be able to know what happened if an error ends up bubbling all the way out of your application.
|
||||
</p>
|
||||
{#code_begin|exe_err#}
|
||||
pub fn main() !void {
|
||||
try foo(12);
|
||||
}
|
||||
|
||||
fn foo(x: i32) !void {
|
||||
if (x >= 5) {
|
||||
try bar();
|
||||
} else {
|
||||
try bang2();
|
||||
}
|
||||
}
|
||||
|
||||
fn bar() !void {
|
||||
if (baz()) {
|
||||
try quux();
|
||||
} else |err| switch (err) {
|
||||
error.FileNotFound => try hello(),
|
||||
else => try another(),
|
||||
}
|
||||
}
|
||||
|
||||
fn baz() !void {
|
||||
try bang1();
|
||||
}
|
||||
|
||||
fn quux() !void {
|
||||
try bang2();
|
||||
}
|
||||
|
||||
fn hello() !void {
|
||||
try bang2();
|
||||
}
|
||||
|
||||
fn another() !void {
|
||||
try bang1();
|
||||
}
|
||||
|
||||
fn bang1() !void {
|
||||
return error.FileNotFound;
|
||||
}
|
||||
|
||||
fn bang2() !void {
|
||||
return error.PermissionDenied;
|
||||
}
|
||||
{#code_end#}
|
||||
<p>
|
||||
Look closely at this example. This is no stack trace.
|
||||
</p>
|
||||
<p>
|
||||
You can see that the final error bubbled up was <code>PermissionDenied</code>,
|
||||
but the original error that started this whole thing was <code>FileNotFound</code>. In the <code>bar</code> function, the code handles the original error code,
|
||||
and then returns another one, from the switch statement. Error Return Traces make this clear, whereas a stack trace would look like this:
|
||||
</p>
|
||||
{#code_begin|exe_err#}
|
||||
pub fn main() void {
|
||||
foo(12);
|
||||
}
|
||||
|
||||
fn foo(x: i32) void {
|
||||
if (x >= 5) {
|
||||
bar();
|
||||
} else {
|
||||
bang2();
|
||||
}
|
||||
}
|
||||
|
||||
fn bar() void {
|
||||
if (baz()) {
|
||||
quux();
|
||||
} else {
|
||||
hello();
|
||||
}
|
||||
}
|
||||
|
||||
fn baz() bool {
|
||||
return bang1();
|
||||
}
|
||||
|
||||
fn quux() void {
|
||||
bang2();
|
||||
}
|
||||
|
||||
fn hello() void {
|
||||
bang2();
|
||||
}
|
||||
|
||||
fn bang1() bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn bang2() void {
|
||||
@panic("PermissionDenied");
|
||||
}
|
||||
{#code_end#}
|
||||
<p>
|
||||
Here, the stack trace does not explain how the control
|
||||
flow in <code>bar</code> got to the <code>hello()</code> call.
|
||||
One would have to open a debugger or further instrument the application
|
||||
in order to find out. The error return trace, on the other hand,
|
||||
shows exactly how the error bubbled up.
|
||||
</p>
|
||||
<p>
|
||||
This debugging feature makes it easier to iterate quickly on code that
|
||||
robustly handles all error conditions. This means that Zig developers
|
||||
will naturally find themselves writing correct, robust code in order
|
||||
to increase their development pace.
|
||||
</p>
|
||||
<p>
|
||||
Error Return Traces are enabled by default in {#link|Debug#} and {#link|ReleaseSafe#} builds and disabled by default in {#link|ReleaseFast#} and {#link|ReleaseSmall#} builds.
|
||||
</p>
|
||||
<p>
|
||||
There are a few ways to activate this error return tracing feature:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Return an error from main</li>
|
||||
<li>An error makes its way to <code>catch unreachable</code> and you have not overridden the default panic handler</li>
|
||||
<li>Use {#link|errorReturnTrace#} to access the current return trace. You can use <code>std.debug.dumpStackTrace</code> to print it. This function returns comptime-known {#link|null#} when building without error return tracing support.</li>
|
||||
</ul>
|
||||
{#header_open|Implementation Details#}
|
||||
<p>
|
||||
To analyze performance cost, there are two cases:
|
||||
</p>
|
||||
<ul>
|
||||
<li>when no errors are returned</li>
|
||||
<li>when returning errors</li>
|
||||
</ul>
|
||||
<p>
|
||||
For the case when no errors are returned, the cost is a single memory write operation, only in the first non-failable function in the call graph that calls a failable function, i.e. when a function returning <code>void</code> calls a function returning <code>error</code>.
|
||||
This is to initialize this struct in the stack memory:
|
||||
</p>
|
||||
{#code_begin|syntax#}
|
||||
pub const StackTrace = struct {
|
||||
index: usize,
|
||||
instruction_addresses: [N]usize,
|
||||
};
|
||||
{#code_end#}
|
||||
<p>
|
||||
Here, N is the maximum function call depth as determined by call graph analysis. Recursion is ignored and counts for 2.
|
||||
</p>
|
||||
<p>
|
||||
A pointer to <code>StackTrace</code> is passed as a secret parameter to every function that can return an error, but it's always the first parameter, so it can likely sit in a register and stay there.
|
||||
</p>
|
||||
<p>
|
||||
That's it for the path when no errors occur. It's practically free in terms of performance.
|
||||
</p>
|
||||
<p>
|
||||
When generating the code for a function that returns an error, just before the <code>return</code> statement (only for the <code>return</code> statements that return errors), Zig generates a call to this function:
|
||||
</p>
|
||||
{#code_begin|syntax#}
|
||||
// marked as "no-inline" in LLVM IR
|
||||
fn __zig_return_error(stack_trace: *StackTrace) void {
|
||||
stack_trace.instruction_addresses[stack_trace.index] = @returnAddress();
|
||||
stack_trace.index = (stack_trace.index + 1) % N;
|
||||
}
|
||||
{#code_end#}
|
||||
<p>
|
||||
The cost is 2 math operations plus some memory reads and writes. The memory accessed is constrained and should remain cached for the duration of the error return bubbling.
|
||||
</p>
|
||||
<p>
|
||||
As for code size cost, 1 function call before a return statement is no big deal. Even so,
|
||||
I have <a href="https://github.com/ziglang/zig/issues/690">a plan</a> to make the call to
|
||||
<code>__zig_return_error</code> a tail call, which brings the code size cost down to actually zero. What is a return statement in code without error return tracing can become a jump instruction in code with error return tracing.
|
||||
</p>
|
||||
{#header_close#}
|
||||
{#header_close#}
|
||||
{#header_close#}
|
||||
{#header_open|Optionals#}
|
||||
@ -3342,6 +3517,15 @@ test "optional type" {
|
||||
// Use compile-time reflection to access the child type of the optional:
|
||||
comptime assert(@typeOf(foo).Child == i32);
|
||||
}
|
||||
{#code_end#}
|
||||
{#header_close#}
|
||||
{#header_open|null#}
|
||||
<p>
|
||||
Just like {#link|undefined#}, <code>null</code> has its own type, and the only way to use it is to
|
||||
cast it to a different type:
|
||||
</p>
|
||||
{#code_begin|syntax#}
|
||||
const optional_value: ?i32 = null;
|
||||
{#code_end#}
|
||||
{#header_close#}
|
||||
{#header_close#}
|
||||
@ -5426,12 +5610,13 @@ pub const TypeInfo = union(TypeId) {
|
||||
{#header_close#}
|
||||
{#header_open|Build Mode#}
|
||||
<p>
|
||||
Zig has three build modes:
|
||||
Zig has four build modes:
|
||||
</p>
|
||||
<ul>
|
||||
<li>{#link|Debug#} (default)</li>
|
||||
<li>{#link|ReleaseFast#}</li>
|
||||
<li>{#link|ReleaseSafe#}</li>
|
||||
<li>{#link|ReleaseSmall#}</li>
|
||||
</ul>
|
||||
<p>
|
||||
To add standard build options to a <code>build.zig</code> file:
|
||||
@ -5448,14 +5633,16 @@ pub fn build(b: &Builder) void {
|
||||
<p>
|
||||
This causes these options to be available:
|
||||
</p>
|
||||
<pre><code class="shell"> -Drelease-safe=(bool) optimizations on and safety on
|
||||
-Drelease-fast=(bool) optimizations on and safety off</code></pre>
|
||||
<pre><code class="shell"> -Drelease-safe=[bool] optimizations on and safety on
|
||||
-Drelease-fast=[bool] optimizations on and safety off
|
||||
-Drelease-small=[bool] size optimizations on and safety off</code></pre>
|
||||
{#header_open|Debug#}
|
||||
<pre><code class="shell">$ zig build-exe example.zig</code></pre>
|
||||
<ul>
|
||||
<li>Fast compilation speed</li>
|
||||
<li>Safety checks enabled</li>
|
||||
<li>Slow runtime performance</li>
|
||||
<li>Large binary size</li>
|
||||
</ul>
|
||||
{#header_close#}
|
||||
{#header_open|ReleaseFast#}
|
||||
@ -5464,6 +5651,7 @@ pub fn build(b: &Builder) void {
|
||||
<li>Fast runtime performance</li>
|
||||
<li>Safety checks disabled</li>
|
||||
<li>Slow compilation speed</li>
|
||||
<li>Large binary size</li>
|
||||
</ul>
|
||||
{#header_close#}
|
||||
{#header_open|ReleaseSafe#}
|
||||
@ -5472,9 +5660,19 @@ pub fn build(b: &Builder) void {
|
||||
<li>Medium runtime performance</li>
|
||||
<li>Safety checks enabled</li>
|
||||
<li>Slow compilation speed</li>
|
||||
<li>Large binary size</li>
|
||||
</ul>
|
||||
{#see_also|Compile Variables|Zig Build System|Undefined Behavior#}
|
||||
{#header_close#}
|
||||
{#header_open|ReleaseSmall#}
|
||||
<pre><code class="shell">$ zig build-exe example.zig --release-small</code></pre>
|
||||
<ul>
|
||||
<li>Medium runtime performance</li>
|
||||
<li>Safety checks disabled</li>
|
||||
<li>Slow compilation speed</li>
|
||||
<li>Small binary size</li>
|
||||
</ul>
|
||||
{#header_close#}
|
||||
{#see_also|Compile Variables|Zig Build System|Undefined Behavior#}
|
||||
{#header_close#}
|
||||
{#header_open|Undefined Behavior#}
|
||||
<p>
|
||||
@ -5482,7 +5680,7 @@ pub fn build(b: &Builder) void {
|
||||
detected at compile-time, Zig emits an error. Most undefined behavior that
|
||||
cannot be detected at compile-time can be detected at runtime. In these cases,
|
||||
Zig has safety checks. Safety checks can be disabled on a per-block basis
|
||||
with <code>@setRuntimeSafety</code>. The {#link|ReleaseFast#}
|
||||
with {#link|setRuntimeSafety#}. The {#link|ReleaseFast#}
|
||||
build mode disables all safety checks in order to facilitate optimizations.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user