mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 05:44:20 +00:00
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:
parent
5316a00a18
commit
eda3eb1561
3 changed files with 301 additions and 29 deletions
133
src/AstGen.zig
133
src/AstGen.zig
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
12
src/Sema.zig
12
src/Sema.zig
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue