From f0697c28f80d64c544302aea576e41ebc443b41c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 14 Jun 2018 18:12:05 -0400 Subject: [PATCH] langref: docs for error return traces See #367 --- doc/langref.html.in | 214 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 206 insertions(+), 8 deletions(-) diff --git a/doc/langref.html.in b/doc/langref.html.in index 1fccd6e351fe..814de721a694 100644 --- a/doc/langref.html.in +++ b/doc/langref.html.in @@ -590,6 +590,7 @@ test "initialization" { x = 1; } {#code_end#} + {#header_open|undefined#}

Use undefined to leave variables uninitialized:

{#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" {
  • You know with complete certainty it will not return an error, so want to unconditionally unwrap it.
  • You want to take a different action for each possible error.
  • + {#header_open|catch#}

    If you want to provide a default value, you can use the catch binary operator:

    {#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 catch operator must match the unwrapped error union type, or be of type noreturn.

    + {#header_close#} + {#header_open|try#}

    Let's say you wanted to return the error if you got one, otherwise continue with the function logic:

    {#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.

    + {#header_close#}

    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 {

    Finally, you may want to take a different action for every situation. For that, we combine - the if and switch expression: + the {#link|if#} and {#link|switch#} expression:

    {#code_begin|syntax#} fn doAThing(str: []u8) void { @@ -3062,9 +3068,10 @@ fn doAThing(str: []u8) void { } } {#code_end#} + {#header_open|errdefer#}

    The other component to error handling is defer statements. - In addition to an unconditional defer, Zig has errdefer, + In addition to an unconditional {#link|defer#}, Zig has errdefer, which evaluates the deferred expression on block exit path if and only if the function returned with an error from the block.

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

    + {#header_close#}

    A couple of other tidbits about error handling:

    @@ -3223,7 +3231,174 @@ test "inferred error set" { {#header_close#} {#header_close#} {#header_open|Error Return Traces#} -

    TODO

    +

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

    + {#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#} +

    + Look closely at this example. This is no stack trace. +

    +

    + You can see that the final error bubbled up was PermissionDenied, + but the original error that started this whole thing was FileNotFound. In the bar 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: +

    + {#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#} +

    + Here, the stack trace does not explain how the control + flow in bar got to the hello() 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. +

    +

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

    +

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

    +

    + There are a few ways to activate this error return tracing feature: +

    + + {#header_open|Implementation Details#} +

    + To analyze performance cost, there are two cases: +

    + +

    + 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 void calls a function returning error. + This is to initialize this struct in the stack memory: +

    + {#code_begin|syntax#} +pub const StackTrace = struct { + index: usize, + instruction_addresses: [N]usize, +}; + {#code_end#} +

    + Here, N is the maximum function call depth as determined by call graph analysis. Recursion is ignored and counts for 2. +

    +

    + A pointer to StackTrace 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. +

    +

    + That's it for the path when no errors occur. It's practically free in terms of performance. +

    +

    + When generating the code for a function that returns an error, just before the return statement (only for the return statements that return errors), Zig generates a call to this function: +

    + {#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#} +

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

    +

    + As for code size cost, 1 function call before a return statement is no big deal. Even so, + I have a plan to make the call to + __zig_return_error 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. +

    + {#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#} +

    + Just like {#link|undefined#}, null has its own type, and the only way to use it is to + cast it to a different type: +

    + {#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#}

    - Zig has three build modes: + Zig has four build modes:

    To add standard build options to a build.zig file: @@ -5448,14 +5633,16 @@ pub fn build(b: &Builder) void {

    This causes these options to be available:

    -
      -Drelease-safe=(bool)  optimizations on and safety on
    -  -Drelease-fast=(bool)  optimizations on and safety off
    +
      -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
    {#header_open|Debug#}
    $ zig build-exe example.zig
    {#header_close#} {#header_open|ReleaseFast#} @@ -5464,6 +5651,7 @@ pub fn build(b: &Builder) void {
  • Fast runtime performance
  • Safety checks disabled
  • Slow compilation speed
  • +
  • Large binary size
  • {#header_close#} {#header_open|ReleaseSafe#} @@ -5472,9 +5660,19 @@ pub fn build(b: &Builder) void {
  • Medium runtime performance
  • Safety checks enabled
  • Slow compilation speed
  • +
  • Large binary size
  • - {#see_also|Compile Variables|Zig Build System|Undefined Behavior#} {#header_close#} + {#header_open|ReleaseSmall#} +
    $ zig build-exe example.zig --release-small
    + + {#header_close#} + {#see_also|Compile Variables|Zig Build System|Undefined Behavior#} {#header_close#} {#header_open|Undefined Behavior#}

    @@ -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 @setRuntimeSafety. The {#link|ReleaseFast#} + with {#link|setRuntimeSafety#}. The {#link|ReleaseFast#} build mode disables all safety checks in order to facilitate optimizations.