Skip to content

Commit

Permalink
langref: docs for error return traces
Browse files Browse the repository at this point in the history
See #367
  • Loading branch information
andrewrk committed Jun 14, 2018
1 parent cdf1e36 commit f0697c2
Showing 1 changed file with 206 additions and 8 deletions.
214 changes: 206 additions & 8 deletions doc/langref.html.in
Expand Up @@ -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;
Expand All @@ -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#}
Expand Down Expand Up @@ -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 {
Expand All @@ -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#}
Expand All @@ -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:
Expand All @@ -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 {
Expand All @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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#}
Expand Down Expand Up @@ -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#}
Expand Down Expand Up @@ -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:
Expand All @@ -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#}
Expand All @@ -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#}
Expand All @@ -5472,17 +5660,27 @@ 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>
Zig has many instances of undefined behavior. If undefined behavior is
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>
Expand Down

0 comments on commit f0697c2

Please sign in to comment.