stage2: "Pop" error trace for break/return within catch

This implement trace "popping" for correctly handled errors within
`catch { ... }` and `else { ... }` blocks.

When breaking from these blocks with any non-error, we pop the error
trace frames corresponding to the operand. When breaking with an error,
we preserve the frames so that error traces "chain" together as usual.

```zig
fn foo(cond1: bool, cond2: bool) !void {
    bar() catch {
    	if (cond1) {
	    // If baz() result is a non-error, pop the error trace frames from bar()
	    // If baz() result is an error, leave the bar() frames on the error trace
            return baz();
	} else if (cond2) {
	    // If we break/return an error, then leave the error frames from bar() on the error trace
	    return error.Foo;
	}
    };

    // An error returned from here does not include bar()'s error frames in the trace
    return error.Bar;
}
```

Notice that if foo() does not return an error it, it leaves no extra
frames on the error trace.

This is piece (1/3) of https://github.com/ziglang/zig/issues/1923#issuecomment-1218495574
This commit is contained in:
Cody Tapscott 2022-09-12 23:05:50 -07:00
parent 5316a00a18
commit eda3eb1561
3 changed files with 301 additions and 29 deletions

View File

@ -1834,6 +1834,13 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
const break_label = node_datas[node].lhs;
const rhs = node_datas[node].rhs;
// Breaking out of a `catch { ... }` or `else |err| { ... }` block with a non-error value
// means that the corresponding error was correctly handled, and the error trace index
// needs to be restored so that any entries from the caught error are effectively "popped"
//
// Note: We only restore for the outermost block, since that will "pop" any nested blocks.
var err_trace_index_to_restore: Zir.Inst.Ref = .none;
// Look for the label in the scope.
var scope = parent_scope;
while (true) {
@ -1842,6 +1849,7 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
const block_gz = scope.cast(GenZir).?;
if (block_gz.cur_defer_node != 0) {
// We are breaking out of a `defer` block.
return astgen.failNodeNotes(node, "cannot break out of defer expression", .{}, &.{
try astgen.errNoteNode(
block_gz.cur_defer_node,
@ -1851,6 +1859,11 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
});
}
if (block_gz.saved_err_trace_index != .none) {
// We are breaking out of a `catch { ... }` or `else |err| { ... }`.
err_trace_index_to_restore = block_gz.saved_err_trace_index;
}
const block_inst = blk: {
if (break_label != 0) {
if (block_gz.label) |*label| {
@ -1862,9 +1875,11 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
} else if (block_gz.break_block != 0) {
break :blk block_gz.break_block;
}
// If not the target, start over with the parent
scope = block_gz.parent;
continue;
};
// If we made it here, this block is the target of the break expr
const break_tag: Zir.Inst.Tag = if (block_gz.is_inline or block_gz.force_comptime)
.break_inline
@ -1874,6 +1889,19 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
if (rhs == 0) {
try genDefers(parent_gz, scope, parent_scope, .normal_only);
// As our last action before the break, "pop" the error trace if needed
if (err_trace_index_to_restore != .none) {
// TODO: error-liveness and is_non_err
_ = try parent_gz.add(.{
.tag = .restore_err_ret_index,
.data = .{ .un_node = .{
.operand = err_trace_index_to_restore,
.src_node = parent_gz.nodeIndexToRelative(node),
} },
});
}
_ = try parent_gz.addBreak(break_tag, block_inst, .void_value);
return Zir.Inst.Ref.unreachable_value;
}
@ -1884,6 +1912,19 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
try genDefers(parent_gz, scope, parent_scope, .normal_only);
// As our last action before the break, "pop" the error trace if needed
if (err_trace_index_to_restore != .none) {
// TODO: error-liveness and is_non_err
_ = try parent_gz.add(.{
.tag = .restore_err_ret_index,
.data = .{ .un_node = .{
.operand = err_trace_index_to_restore,
.src_node = parent_gz.nodeIndexToRelative(node),
} },
});
}
switch (block_gz.break_result_loc) {
.block_ptr => {
const br = try parent_gz.addBreak(break_tag, block_inst, operand);
@ -5160,9 +5201,7 @@ fn orelseCatchExpr(
block_scope.setBreakResultLoc(rl);
defer block_scope.unstack();
if (do_err_trace) {
block_scope.saved_err_trace_index = try parent_gz.addNode(.save_err_ret_index, node);
}
const saved_err_trace_index = if (do_err_trace) try parent_gz.addNode(.save_err_ret_index, node) else .none;
const operand_rl: ResultLoc = switch (block_scope.break_result_loc) {
.ref => .ref,
@ -5195,6 +5234,12 @@ fn orelseCatchExpr(
var else_scope = block_scope.makeSubBlock(scope);
defer else_scope.unstack();
// Any break (of a non-error value) that navigates out of this scope means
// that the error was handled successfully, so this index will be restored.
else_scope.saved_err_trace_index = saved_err_trace_index;
if (else_scope.outermost_err_trace_index == .none)
else_scope.outermost_err_trace_index = saved_err_trace_index;
var err_val_scope: Scope.LocalVal = undefined;
const else_sub_scope = blk: {
const payload = payload_token orelse break :blk &else_scope.base;
@ -5220,6 +5265,17 @@ fn orelseCatchExpr(
const else_result = try expr(&else_scope, else_sub_scope, block_scope.break_result_loc, rhs);
if (!else_scope.endsWithNoReturn()) {
block_scope.break_count += 1;
// TODO: Add is_non_err and break check
if (do_err_trace) {
_ = try else_scope.add(.{
.tag = .restore_err_ret_index,
.data = .{ .un_node = .{
.operand = saved_err_trace_index,
.src_node = parent_gz.nodeIndexToRelative(node),
} },
});
}
}
try checkUsed(parent_gz, &else_scope.base, else_sub_scope);
@ -5243,15 +5299,6 @@ fn orelseCatchExpr(
block,
break_tag,
);
if (do_err_trace) {
_ = try parent_gz.add(.{
.tag = .restore_err_ret_index,
.data = .{ .un_node = .{
.operand = parent_gz.saved_err_trace_index,
.src_node = parent_gz.nodeIndexToRelative(node),
} },
});
}
return result;
}
@ -5454,9 +5501,7 @@ fn ifExpr(
block_scope.setBreakResultLoc(rl);
defer block_scope.unstack();
if (do_err_trace) {
block_scope.saved_err_trace_index = try parent_gz.addNode(.save_err_ret_index, node);
}
const saved_err_trace_index = if (do_err_trace) try parent_gz.addNode(.save_err_ret_index, node) else .none;
const payload_is_ref = if (if_full.payload_token) |payload_token|
token_tags[payload_token] == .asterisk
@ -5574,6 +5619,12 @@ fn ifExpr(
var else_scope = parent_gz.makeSubBlock(scope);
defer else_scope.unstack();
// Any break (of a non-error value) that navigates out of this scope means
// that the error was handled successfully, so this index will be restored.
else_scope.saved_err_trace_index = saved_err_trace_index;
if (else_scope.outermost_err_trace_index == .none)
else_scope.outermost_err_trace_index = saved_err_trace_index;
const else_node = if_full.ast.else_expr;
const else_info: struct {
src: Ast.Node.Index,
@ -5625,6 +5676,18 @@ fn ifExpr(
},
};
if (do_err_trace and !else_scope.endsWithNoReturn()) {
// TODO: is_non_err and other checks
_ = try else_scope.add(.{
.tag = .restore_err_ret_index,
.data = .{ .un_node = .{
.operand = saved_err_trace_index,
.src_node = parent_gz.nodeIndexToRelative(node),
} },
});
}
const break_tag: Zir.Inst.Tag = if (parent_gz.force_comptime) .break_inline else .@"break";
const result = try finishThenElseBlock(
parent_gz,
@ -5641,15 +5704,6 @@ fn ifExpr(
block,
break_tag,
);
if (do_err_trace) {
_ = try parent_gz.add(.{
.tag = .restore_err_ret_index,
.data = .{ .un_node = .{
.operand = parent_gz.saved_err_trace_index,
.src_node = parent_gz.nodeIndexToRelative(node),
} },
});
}
return result;
}
@ -6780,11 +6834,24 @@ fn ret(gz: *GenZir, scope: *Scope, node: Ast.Node.Index) InnerError!Zir.Inst.Ref
const operand = try reachableExpr(gz, scope, rl, operand_node, node);
gz.anon_name_strategy = prev_anon_name_strategy;
// TODO: This should be almost identical for every break/ret
switch (nodeMayEvalToError(tree, operand_node)) {
.never => {
// Returning a value that cannot be an error; skip error defers.
try genDefers(gz, defer_outer, scope, .normal_only);
try emitDbgStmt(gz, ret_line, ret_column);
// As our last action before the return, "pop" the error trace if needed
if (gz.outermost_err_trace_index != .none) {
_ = try gz.add(.{
.tag = .restore_err_ret_index,
.data = .{ .un_node = .{
.operand = gz.outermost_err_trace_index,
.src_node = gz.nodeIndexToRelative(node),
} },
});
}
try gz.addRet(rl, operand, node);
return Zir.Inst.Ref.unreachable_value;
},
@ -6826,6 +6893,17 @@ fn ret(gz: *GenZir, scope: *Scope, node: Ast.Node.Index) InnerError!Zir.Inst.Ref
};
try genDefers(&else_scope, defer_outer, scope, which_ones);
try emitDbgStmt(&else_scope, ret_line, ret_column);
// As our last action before the return, "pop" the error trace if needed
if (else_scope.outermost_err_trace_index != .none) {
_ = try else_scope.add(.{
.tag = .restore_err_ret_index,
.data = .{ .un_node = .{
.operand = else_scope.outermost_err_trace_index,
.src_node = else_scope.nodeIndexToRelative(node),
} },
});
}
try else_scope.addRet(rl, operand, node);
try setCondBrPayload(condbr, is_non_err, &then_scope, 0, &else_scope, 0);
@ -10334,7 +10412,12 @@ const GenZir = struct {
/// Keys are the raw instruction index, values are the closure_capture instruction.
captures: std.AutoHashMapUnmanaged(Zir.Inst.Index, Zir.Inst.Index) = .{},
/// If this GenZir corresponds to a `catch { ... }` or `else |err| { ... }` block,
/// this err_trace_index can be restored to "pop" the trace entries for the block.
saved_err_trace_index: Zir.Inst.Ref = .none,
/// When returning from a function with a non-error, we must pop all trace entries
/// from any containing `catch { ... }` or `else |err| { ... }` blocks.
outermost_err_trace_index: Zir.Inst.Ref = .none,
const unstacked_top = std.math.maxInt(usize);
/// Call unstack before adding any new instructions to containing GenZir.
@ -10380,7 +10463,7 @@ const GenZir = struct {
.any_defer_node = gz.any_defer_node,
.instructions = gz.instructions,
.instructions_top = gz.instructions.items.len,
.saved_err_trace_index = gz.saved_err_trace_index,
.outermost_err_trace_index = gz.outermost_err_trace_index,
};
}

View File

@ -16190,9 +16190,14 @@ fn zirSaveErrRetIndex(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileE
// This is only relevant at runtime.
if (block.is_comptime) return Air.Inst.Ref.zero_usize;
// In the corner case that `catch { ... }` or `else |err| { ... }` is used in a function
// that does *not* make any errorable calls, we still need an error trace to interact with
// the AIR instructions we've already emitted.
if (sema.owner_func != null)
sema.owner_func.?.calls_or_awaits_errorable_fn = true;
const backend_supports_error_return_tracing = sema.mod.comp.bin_file.options.use_llvm;
const ok = sema.owner_func.?.calls_or_awaits_errorable_fn and
sema.mod.comp.bin_file.options.error_return_tracing and
const ok = sema.mod.comp.bin_file.options.error_return_tracing and
backend_supports_error_return_tracing;
if (!ok) return Air.Inst.Ref.zero_usize;
@ -16211,8 +16216,7 @@ fn zirRestoreErrRetIndex(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Compi
if (block.is_comptime) return;
const backend_supports_error_return_tracing = sema.mod.comp.bin_file.options.use_llvm;
const ok = sema.owner_func.?.calls_or_awaits_errorable_fn and
sema.mod.comp.bin_file.options.error_return_tracing and
const ok = sema.mod.comp.bin_file.options.error_return_tracing and
backend_supports_error_return_tracing;
if (!ok) return;

View File

@ -98,6 +98,191 @@ pub fn addCases(cases: *tests.StackTracesContext) void {
},
});
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 = "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 =