zig/test/stack_traces.zig
Cody Tapscott 3007fdde45 stage2: Pop error trace when storing error to var/const
In order to enforce a strict stack discipline for error return traces,
we cannot track error return traces that are stored in variables:

  ```zig
  const x = errorable(); // errorable()'s error return trace is killed here

  // v-- error trace starts here instead
  return x catch error.UnknownError;
  ```

In order to propagate error return traces, function calls need to be passed
directly to an error-handling expression (`if`, `catch`, `try` or `return`):

  ```zig
  // When passed directly to `catch`, the return trace is propagated
  return errorable() catch error.UnknownError;

  // Using a break also works
  return blk: {
      // code here
      break :blk errorable();
  } catch error.UnknownError;
  ```

Why do we need this restriction? Without it, multiple errors can co-exist
with their own error traces. Handling that situation correctly means either:
  a. Dynamically allocating trace memory and tracking lifetimes, OR
  b. Allowing the production of one error to interfere with the trace of another
     (which is the current status quo)

This is piece (3/3) of https://github.com/ziglang/zig/issues/1923#issuecomment-1218495574
2022-10-21 10:44:20 -07:00

495 lines
13 KiB
Zig

const std = @import("std");
const os = std.os;
const tests = @import("tests.zig");
pub fn addCases(cases: *tests.StackTracesContext) void {
cases.addCase(.{
.name = "return",
.source =
\\pub fn main() !void {
\\ return error.TheSkyIsFalling;
\\}
,
.Debug = .{
.expect =
\\error: TheSkyIsFalling
\\source.zig:2:5: [address] in main (test)
\\ return error.TheSkyIsFalling;
\\ ^
\\
,
},
.ReleaseSafe = .{
.exclude_os = .{
.windows, // TODO
.linux, // defeated by aggressive inlining
},
.expect =
\\error: TheSkyIsFalling
\\source.zig:2:5: [address] in [function]
\\ return error.TheSkyIsFalling;
\\ ^
\\
,
},
.ReleaseFast = .{
.expect =
\\error: TheSkyIsFalling
\\
,
},
.ReleaseSmall = .{
.expect =
\\error: TheSkyIsFalling
\\
,
},
});
cases.addCase(.{
.name = "try return",
.source =
\\fn foo() !void {
\\ return error.TheSkyIsFalling;
\\}
\\
\\pub fn main() !void {
\\ try foo();
\\}
,
.Debug = .{
.expect =
\\error: TheSkyIsFalling
\\source.zig:2:5: [address] in foo (test)
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:6:5: [address] in main (test)
\\ try foo();
\\ ^
\\
,
},
.ReleaseSafe = .{
.exclude_os = .{
.windows, // TODO
},
.expect =
\\error: TheSkyIsFalling
\\source.zig:2:5: [address] in [function]
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:6:5: [address] in [function]
\\ try foo();
\\ ^
\\
,
},
.ReleaseFast = .{
.expect =
\\error: TheSkyIsFalling
\\
,
},
.ReleaseSmall = .{
.expect =
\\error: TheSkyIsFalling
\\
,
},
});
cases.addCase(.{
.name = "try return + handled catch/if-else",
.source =
\\fn foo() !void {
\\ return error.TheSkyIsFalling;
\\}
\\
\\pub fn main() !void {
\\ foo() catch {}; // should not affect error trace
\\ if (foo()) |_| {} else |_| {
\\ // should also not affect error trace
\\ }
\\ try foo();
\\}
,
.Debug = .{
.expect =
\\error: TheSkyIsFalling
\\source.zig:2:5: [address] in foo (test)
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:10:5: [address] in main (test)
\\ try foo();
\\ ^
\\
,
},
.ReleaseSafe = .{
.exclude_os = .{
.windows, // TODO
.linux, // defeated by aggressive inlining
},
.expect =
\\error: TheSkyIsFalling
\\source.zig:2:5: [address] in [function]
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:10:5: [address] in [function]
\\ try foo();
\\ ^
\\
,
},
.ReleaseFast = .{
.expect =
\\error: TheSkyIsFalling
\\
,
},
.ReleaseSmall = .{
.expect =
\\error: TheSkyIsFalling
\\
,
},
});
cases.addCase(.{
.name = "catch and re-throw error",
.source =
\\fn foo() !void {
\\ return error.TheSkyIsFalling;
\\}
\\
\\pub fn main() !void {
\\ return foo() catch error.AndMyCarIsOutOfGas;
\\}
,
.Debug = .{
.expect =
\\error: AndMyCarIsOutOfGas
\\source.zig:2:5: [address] in foo (test)
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:6:5: [address] in main (test)
\\ return foo() catch error.AndMyCarIsOutOfGas;
\\ ^
\\
,
},
.ReleaseSafe = .{
.exclude_os = .{
.windows, // TODO
.linux, // defeated by aggressive inlining
},
.expect =
\\error: AndMyCarIsOutOfGas
\\source.zig:2:5: [address] in [function]
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:6:5: [address] in [function]
\\ return foo() catch error.AndMyCarIsOutOfGas;
\\ ^
\\
,
},
.ReleaseFast = .{
.expect =
\\error: AndMyCarIsOutOfGas
\\
,
},
.ReleaseSmall = .{
.expect =
\\error: AndMyCarIsOutOfGas
\\
,
},
});
cases.addCase(.{
.name = "stored errors do not contribute to error trace",
.source =
\\fn foo() !void {
\\ return error.TheSkyIsFalling;
\\}
\\
\\pub fn main() !void {
\\ // Once an error is stored in a variable, it is popped from the trace
\\ var x = foo();
\\ x = {};
\\
\\ // As a result, this error trace will still be clean
\\ return error.SomethingUnrelatedWentWrong;
\\}
,
.Debug = .{
.expect =
\\error: SomethingUnrelatedWentWrong
\\source.zig:11:5: [address] in main (test)
\\ return error.SomethingUnrelatedWentWrong;
\\ ^
\\
,
},
.ReleaseSafe = .{
.exclude_os = .{
.windows, // TODO
.linux, // defeated by aggressive inlining
},
.expect =
\\error: SomethingUnrelatedWentWrong
\\source.zig:11:5: [address] in [function]
\\ return error.SomethingUnrelatedWentWrong;
\\ ^
\\
,
},
.ReleaseFast = .{
.expect =
\\error: SomethingUnrelatedWentWrong
\\
,
},
.ReleaseSmall = .{
.expect =
\\error: SomethingUnrelatedWentWrong
\\
,
},
});
cases.addCase(.{
.name = "try return from within catch",
.source =
\\fn foo() !void {
\\ return error.TheSkyIsFalling;
\\}
\\
\\fn bar() !void {
\\ return error.AndMyCarIsOutOfGas;
\\}
\\
\\pub fn main() !void {
\\ foo() catch { // error trace should include foo()
\\ try bar();
\\ };
\\}
,
.Debug = .{
.expect =
\\error: AndMyCarIsOutOfGas
\\source.zig:2:5: [address] in foo (test)
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:6:5: [address] in bar (test)
\\ return error.AndMyCarIsOutOfGas;
\\ ^
\\source.zig:11:9: [address] in main (test)
\\ try bar();
\\ ^
\\
,
},
.ReleaseSafe = .{
.exclude_os = .{
.windows, // TODO
},
.expect =
\\error: AndMyCarIsOutOfGas
\\source.zig:2:5: [address] in [function]
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:6:5: [address] in [function]
\\ return error.AndMyCarIsOutOfGas;
\\ ^
\\source.zig:11:9: [address] in [function]
\\ try bar();
\\ ^
\\
,
},
.ReleaseFast = .{
.expect =
\\error: AndMyCarIsOutOfGas
\\
,
},
.ReleaseSmall = .{
.expect =
\\error: AndMyCarIsOutOfGas
\\
,
},
});
cases.addCase(.{
.name = "try return from within if-else",
.source =
\\fn foo() !void {
\\ return error.TheSkyIsFalling;
\\}
\\
\\fn bar() !void {
\\ return error.AndMyCarIsOutOfGas;
\\}
\\
\\pub fn main() !void {
\\ if (foo()) |_| {} else |_| { // error trace should include foo()
\\ try bar();
\\ }
\\}
,
.Debug = .{
.expect =
\\error: AndMyCarIsOutOfGas
\\source.zig:2:5: [address] in foo (test)
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:6:5: [address] in bar (test)
\\ return error.AndMyCarIsOutOfGas;
\\ ^
\\source.zig:11:9: [address] in main (test)
\\ try bar();
\\ ^
\\
,
},
.ReleaseSafe = .{
.exclude_os = .{
.windows, // TODO
},
.expect =
\\error: AndMyCarIsOutOfGas
\\source.zig:2:5: [address] in [function]
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:6:5: [address] in [function]
\\ return error.AndMyCarIsOutOfGas;
\\ ^
\\source.zig:11:9: [address] in [function]
\\ try bar();
\\ ^
\\
,
},
.ReleaseFast = .{
.expect =
\\error: AndMyCarIsOutOfGas
\\
,
},
.ReleaseSmall = .{
.expect =
\\error: AndMyCarIsOutOfGas
\\
,
},
});
cases.addCase(.{
.name = "try try return return",
.source =
\\fn foo() !void {
\\ try bar();
\\}
\\
\\fn bar() !void {
\\ return make_error();
\\}
\\
\\fn make_error() !void {
\\ return error.TheSkyIsFalling;
\\}
\\
\\pub fn main() !void {
\\ try foo();
\\}
,
.Debug = .{
.expect =
\\error: TheSkyIsFalling
\\source.zig:10:5: [address] in make_error (test)
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:6:5: [address] in bar (test)
\\ return make_error();
\\ ^
\\source.zig:2:5: [address] in foo (test)
\\ try bar();
\\ ^
\\source.zig:14:5: [address] in main (test)
\\ try foo();
\\ ^
\\
,
},
.ReleaseSafe = .{
.exclude_os = .{
.windows, // TODO
},
.expect =
\\error: TheSkyIsFalling
\\source.zig:10:5: [address] in [function]
\\ return error.TheSkyIsFalling;
\\ ^
\\source.zig:6:5: [address] in [function]
\\ return make_error();
\\ ^
\\source.zig:2:5: [address] in [function]
\\ try bar();
\\ ^
\\source.zig:14:5: [address] in [function]
\\ try foo();
\\ ^
\\
,
},
.ReleaseFast = .{
.expect =
\\error: TheSkyIsFalling
\\
,
},
.ReleaseSmall = .{
.expect =
\\error: TheSkyIsFalling
\\
,
},
});
cases.addCase(.{
.exclude_os = .{
.openbsd, // integer overflow
.windows, // TODO intermittent failures
},
.name = "dumpCurrentStackTrace",
.source =
\\const std = @import("std");
\\
\\fn bar() void {
\\ std.debug.dumpCurrentStackTrace(@returnAddress());
\\}
\\fn foo() void {
\\ bar();
\\}
\\pub fn main() u8 {
\\ foo();
\\ return 1;
\\}
,
.Debug = .{
.expect =
\\source.zig:7:8: [address] in foo (test)
\\ bar();
\\ ^
\\source.zig:10:8: [address] in main (test)
\\ foo();
\\ ^
\\
,
},
});
}