authorgravatar for topolarity@tapscott.meCody Tapscott <topolarity@tapscott.me> 2022-09-12 23:05:50-07:00
committergravatar for topolarity@tapscott.meCody Tapscott <topolarity@tapscott.me> 2022-10-21 10:43:42-07:00
logeda3eb1561ec9a68e692821d5de71d03e6f50d42
tree5a3acd315089bfda58cf4366445ae8ca1916e5fd
parent5316a00a188955d60cc38d56def51b8605181225

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

3 files changed, 301 insertions(+), 29 deletions(-)

src/AstGen.zig+108-25
......@@ -1834,6 +1834,13 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
18341834 const break_label = node_datas[node].lhs;
18351835 const rhs = node_datas[node].rhs;
18361836
1837 // Breaking out of a `catch { ... }` or `else |err| { ... }` block with a non-error value
1838 // means that the corresponding error was correctly handled, and the error trace index
1839 // needs to be restored so that any entries from the caught error are effectively "popped"
1840 //
1841 // Note: We only restore for the outermost block, since that will "pop" any nested blocks.
1842 var err_trace_index_to_restore: Zir.Inst.Ref = .none;
1843
18371844 // Look for the label in the scope.
18381845 var scope = parent_scope;
18391846 while (true) {
......@@ -1842,6 +1849,7 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
18421849 const block_gz = scope.cast(GenZir).?;
18431850
18441851 if (block_gz.cur_defer_node != 0) {
1852 // We are breaking out of a `defer` block.
18451853 return astgen.failNodeNotes(node, "cannot break out of defer expression", .{}, &.{
18461854 try astgen.errNoteNode(
18471855 block_gz.cur_defer_node,
......@@ -1851,6 +1859,11 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
18511859 });
18521860 }
18531861
1862 if (block_gz.saved_err_trace_index != .none) {
1863 // We are breaking out of a `catch { ... }` or `else |err| { ... }`.
1864 err_trace_index_to_restore = block_gz.saved_err_trace_index;
1865 }
1866
18541867 const block_inst = blk: {
18551868 if (break_label != 0) {
18561869 if (block_gz.label) |*label| {
......@@ -1862,9 +1875,11 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
18621875 } else if (block_gz.break_block != 0) {
18631876 break :blk block_gz.break_block;
18641877 }
1878 // If not the target, start over with the parent
18651879 scope = block_gz.parent;
18661880 continue;
18671881 };
1882 // If we made it here, this block is the target of the break expr
18681883
18691884 const break_tag: Zir.Inst.Tag = if (block_gz.is_inline or block_gz.force_comptime)
18701885 .break_inline
......@@ -1874,6 +1889,19 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
18741889 if (rhs == 0) {
18751890 try genDefers(parent_gz, scope, parent_scope, .normal_only);
18761891
1892 // As our last action before the break, "pop" the error trace if needed
1893 if (err_trace_index_to_restore != .none) {
1894 // TODO: error-liveness and is_non_err
1895
1896 _ = try parent_gz.add(.{
1897 .tag = .restore_err_ret_index,
1898 .data = .{ .un_node = .{
1899 .operand = err_trace_index_to_restore,
1900 .src_node = parent_gz.nodeIndexToRelative(node),
1901 } },
1902 });
1903 }
1904
18771905 _ = try parent_gz.addBreak(break_tag, block_inst, .void_value);
18781906 return Zir.Inst.Ref.unreachable_value;
18791907 }
......@@ -1884,6 +1912,19 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
18841912
18851913 try genDefers(parent_gz, scope, parent_scope, .normal_only);
18861914
1915 // As our last action before the break, "pop" the error trace if needed
1916 if (err_trace_index_to_restore != .none) {
1917 // TODO: error-liveness and is_non_err
1918
1919 _ = try parent_gz.add(.{
1920 .tag = .restore_err_ret_index,
1921 .data = .{ .un_node = .{
1922 .operand = err_trace_index_to_restore,
1923 .src_node = parent_gz.nodeIndexToRelative(node),
1924 } },
1925 });
1926 }
1927
18871928 switch (block_gz.break_result_loc) {
18881929 .block_ptr => {
18891930 const br = try parent_gz.addBreak(break_tag, block_inst, operand);
......@@ -5160,9 +5201,7 @@ fn orelseCatchExpr(
51605201 block_scope.setBreakResultLoc(rl);
51615202 defer block_scope.unstack();
51625203
5163 if (do_err_trace) {
5164 block_scope.saved_err_trace_index = try parent_gz.addNode(.save_err_ret_index, node);
5165 }
5204 const saved_err_trace_index = if (do_err_trace) try parent_gz.addNode(.save_err_ret_index, node) else .none;
51665205
51675206 const operand_rl: ResultLoc = switch (block_scope.break_result_loc) {
51685207 .ref => .ref,
......@@ -5195,6 +5234,12 @@ fn orelseCatchExpr(
51955234 var else_scope = block_scope.makeSubBlock(scope);
51965235 defer else_scope.unstack();
51975236
5237 // Any break (of a non-error value) that navigates out of this scope means
5238 // that the error was handled successfully, so this index will be restored.
5239 else_scope.saved_err_trace_index = saved_err_trace_index;
5240 if (else_scope.outermost_err_trace_index == .none)
5241 else_scope.outermost_err_trace_index = saved_err_trace_index;
5242
51985243 var err_val_scope: Scope.LocalVal = undefined;
51995244 const else_sub_scope = blk: {
52005245 const payload = payload_token orelse break :blk &else_scope.base;
......@@ -5220,6 +5265,17 @@ fn orelseCatchExpr(
52205265 const else_result = try expr(&else_scope, else_sub_scope, block_scope.break_result_loc, rhs);
52215266 if (!else_scope.endsWithNoReturn()) {
52225267 block_scope.break_count += 1;
5268
5269 // TODO: Add is_non_err and break check
5270 if (do_err_trace) {
5271 _ = try else_scope.add(.{
5272 .tag = .restore_err_ret_index,
5273 .data = .{ .un_node = .{
5274 .operand = saved_err_trace_index,
5275 .src_node = parent_gz.nodeIndexToRelative(node),
5276 } },
5277 });
5278 }
52235279 }
52245280 try checkUsed(parent_gz, &else_scope.base, else_sub_scope);
52255281
......@@ -5243,15 +5299,6 @@ fn orelseCatchExpr(
52435299 block,
52445300 break_tag,
52455301 );
5246 if (do_err_trace) {
5247 _ = try parent_gz.add(.{
5248 .tag = .restore_err_ret_index,
5249 .data = .{ .un_node = .{
5250 .operand = parent_gz.saved_err_trace_index,
5251 .src_node = parent_gz.nodeIndexToRelative(node),
5252 } },
5253 });
5254 }
52555302 return result;
52565303}
52575304
......@@ -5454,9 +5501,7 @@ fn ifExpr(
54545501 block_scope.setBreakResultLoc(rl);
54555502 defer block_scope.unstack();
54565503
5457 if (do_err_trace) {
5458 block_scope.saved_err_trace_index = try parent_gz.addNode(.save_err_ret_index, node);
5459 }
5504 const saved_err_trace_index = if (do_err_trace) try parent_gz.addNode(.save_err_ret_index, node) else .none;
54605505
54615506 const payload_is_ref = if (if_full.payload_token) |payload_token|
54625507 token_tags[payload_token] == .asterisk
......@@ -5574,6 +5619,12 @@ fn ifExpr(
55745619 var else_scope = parent_gz.makeSubBlock(scope);
55755620 defer else_scope.unstack();
55765621
5622 // Any break (of a non-error value) that navigates out of this scope means
5623 // that the error was handled successfully, so this index will be restored.
5624 else_scope.saved_err_trace_index = saved_err_trace_index;
5625 if (else_scope.outermost_err_trace_index == .none)
5626 else_scope.outermost_err_trace_index = saved_err_trace_index;
5627
55775628 const else_node = if_full.ast.else_expr;
55785629 const else_info: struct {
55795630 src: Ast.Node.Index,
......@@ -5625,6 +5676,18 @@ fn ifExpr(
56255676 },
56265677 };
56275678
5679 if (do_err_trace and !else_scope.endsWithNoReturn()) {
5680 // TODO: is_non_err and other checks
5681
5682 _ = try else_scope.add(.{
5683 .tag = .restore_err_ret_index,
5684 .data = .{ .un_node = .{
5685 .operand = saved_err_trace_index,
5686 .src_node = parent_gz.nodeIndexToRelative(node),
5687 } },
5688 });
5689 }
5690
56285691 const break_tag: Zir.Inst.Tag = if (parent_gz.force_comptime) .break_inline else .@"break";
56295692 const result = try finishThenElseBlock(
56305693 parent_gz,
......@@ -5641,15 +5704,6 @@ fn ifExpr(
56415704 block,
56425705 break_tag,
56435706 );
5644 if (do_err_trace) {
5645 _ = try parent_gz.add(.{
5646 .tag = .restore_err_ret_index,
5647 .data = .{ .un_node = .{
5648 .operand = parent_gz.saved_err_trace_index,
5649 .src_node = parent_gz.nodeIndexToRelative(node),
5650 } },
5651 });
5652 }
56535707 return result;
56545708}
56555709
......@@ -6780,11 +6834,24 @@ fn ret(gz: *GenZir, scope: *Scope, node: Ast.Node.Index) InnerError!Zir.Inst.Ref
67806834 const operand = try reachableExpr(gz, scope, rl, operand_node, node);
67816835 gz.anon_name_strategy = prev_anon_name_strategy;
67826836
6837 // TODO: This should be almost identical for every break/ret
67836838 switch (nodeMayEvalToError(tree, operand_node)) {
67846839 .never => {
67856840 // Returning a value that cannot be an error; skip error defers.
67866841 try genDefers(gz, defer_outer, scope, .normal_only);
67876842 try emitDbgStmt(gz, ret_line, ret_column);
6843
6844 // As our last action before the return, "pop" the error trace if needed
6845 if (gz.outermost_err_trace_index != .none) {
6846 _ = try gz.add(.{
6847 .tag = .restore_err_ret_index,
6848 .data = .{ .un_node = .{
6849 .operand = gz.outermost_err_trace_index,
6850 .src_node = gz.nodeIndexToRelative(node),
6851 } },
6852 });
6853 }
6854
67886855 try gz.addRet(rl, operand, node);
67896856 return Zir.Inst.Ref.unreachable_value;
67906857 },
......@@ -6826,6 +6893,17 @@ fn ret(gz: *GenZir, scope: *Scope, node: Ast.Node.Index) InnerError!Zir.Inst.Ref
68266893 };
68276894 try genDefers(&else_scope, defer_outer, scope, which_ones);
68286895 try emitDbgStmt(&else_scope, ret_line, ret_column);
6896
6897 // As our last action before the return, "pop" the error trace if needed
6898 if (else_scope.outermost_err_trace_index != .none) {
6899 _ = try else_scope.add(.{
6900 .tag = .restore_err_ret_index,
6901 .data = .{ .un_node = .{
6902 .operand = else_scope.outermost_err_trace_index,
6903 .src_node = else_scope.nodeIndexToRelative(node),
6904 } },
6905 });
6906 }
68296907 try else_scope.addRet(rl, operand, node);
68306908
68316909 try setCondBrPayload(condbr, is_non_err, &then_scope, 0, &else_scope, 0);
......@@ -10334,7 +10412,12 @@ const GenZir = struct {
1033410412 /// Keys are the raw instruction index, values are the closure_capture instruction.
1033510413 captures: std.AutoHashMapUnmanaged(Zir.Inst.Index, Zir.Inst.Index) = .{},
1033610414
10415 /// If this GenZir corresponds to a `catch { ... }` or `else |err| { ... }` block,
10416 /// this err_trace_index can be restored to "pop" the trace entries for the block.
1033710417 saved_err_trace_index: Zir.Inst.Ref = .none,
10418 /// When returning from a function with a non-error, we must pop all trace entries
10419 /// from any containing `catch { ... }` or `else |err| { ... }` blocks.
10420 outermost_err_trace_index: Zir.Inst.Ref = .none,
1033810421
1033910422 const unstacked_top = std.math.maxInt(usize);
1034010423 /// Call unstack before adding any new instructions to containing GenZir.
......@@ -10380,7 +10463,7 @@ const GenZir = struct {
1038010463 .any_defer_node = gz.any_defer_node,
1038110464 .instructions = gz.instructions,
1038210465 .instructions_top = gz.instructions.items.len,
10383 .saved_err_trace_index = gz.saved_err_trace_index,
10466 .outermost_err_trace_index = gz.outermost_err_trace_index,
1038410467 };
1038510468 }
1038610469
src/Sema.zig+8-4
......@@ -16190,9 +16190,14 @@ fn zirSaveErrRetIndex(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileE
1619016190 // This is only relevant at runtime.
1619116191 if (block.is_comptime) return Air.Inst.Ref.zero_usize;
1619216192
16193 // In the corner case that `catch { ... }` or `else |err| { ... }` is used in a function
16194 // that does *not* make any errorable calls, we still need an error trace to interact with
16195 // the AIR instructions we've already emitted.
16196 if (sema.owner_func != null)
16197 sema.owner_func.?.calls_or_awaits_errorable_fn = true;
16198
1619316199 const backend_supports_error_return_tracing = sema.mod.comp.bin_file.options.use_llvm;
16194 const ok = sema.owner_func.?.calls_or_awaits_errorable_fn and
16195 sema.mod.comp.bin_file.options.error_return_tracing and
16200 const ok = sema.mod.comp.bin_file.options.error_return_tracing and
1619616201 backend_supports_error_return_tracing;
1619716202 if (!ok) return Air.Inst.Ref.zero_usize;
1619816203
......@@ -16211,8 +16216,7 @@ fn zirRestoreErrRetIndex(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Compi
1621116216 if (block.is_comptime) return;
1621216217
1621316218 const backend_supports_error_return_tracing = sema.mod.comp.bin_file.options.use_llvm;
16214 const ok = sema.owner_func.?.calls_or_awaits_errorable_fn and
16215 sema.mod.comp.bin_file.options.error_return_tracing and
16219 const ok = sema.mod.comp.bin_file.options.error_return_tracing and
1621616220 backend_supports_error_return_tracing;
1621716221 if (!ok) return;
1621816222
test/stack_traces.zig+185
......@@ -98,6 +98,191 @@ pub fn addCases(cases: *tests.StackTracesContext) void {
9898 },
9999 });
100100
101 cases.addCase(.{
102 .name = "try return + handled catch/if-else",
103 .source =
104 \\fn foo() !void {
105 \\ return error.TheSkyIsFalling;
106 \\}
107 \\
108 \\pub fn main() !void {
109 \\ foo() catch {}; // should not affect error trace
110 \\ if (foo()) |_| {} else |_| {
111 \\ // should also not affect error trace
112 \\ }
113 \\ try foo();
114 \\}
115 ,
116 .Debug = .{
117 .expect =
118 \\error: TheSkyIsFalling
119 \\source.zig:2:5: [address] in foo (test)
120 \\ return error.TheSkyIsFalling;
121 \\ ^
122 \\source.zig:10:5: [address] in main (test)
123 \\ try foo();
124 \\ ^
125 \\
126 ,
127 },
128 .ReleaseSafe = .{
129 .exclude_os = .{
130 .windows, // TODO
131 .linux, // defeated by aggressive inlining
132 },
133 .expect =
134 \\error: TheSkyIsFalling
135 \\source.zig:2:5: [address] in [function]
136 \\ return error.TheSkyIsFalling;
137 \\ ^
138 \\source.zig:10:5: [address] in [function]
139 \\ try foo();
140 \\ ^
141 \\
142 ,
143 },
144 .ReleaseFast = .{
145 .expect =
146 \\error: TheSkyIsFalling
147 \\
148 ,
149 },
150 .ReleaseSmall = .{
151 .expect =
152 \\error: TheSkyIsFalling
153 \\
154 ,
155 },
156 });
157
158 cases.addCase(.{
159 .name = "try return from within catch",
160 .source =
161 \\fn foo() !void {
162 \\ return error.TheSkyIsFalling;
163 \\}
164 \\
165 \\fn bar() !void {
166 \\ return error.AndMyCarIsOutOfGas;
167 \\}
168 \\
169 \\pub fn main() !void {
170 \\ foo() catch { // error trace should include foo()
171 \\ try bar();
172 \\ };
173 \\}
174 ,
175 .Debug = .{
176 .expect =
177 \\error: AndMyCarIsOutOfGas
178 \\source.zig:2:5: [address] in foo (test)
179 \\ return error.TheSkyIsFalling;
180 \\ ^
181 \\source.zig:6:5: [address] in bar (test)
182 \\ return error.AndMyCarIsOutOfGas;
183 \\ ^
184 \\source.zig:11:9: [address] in main (test)
185 \\ try bar();
186 \\ ^
187 \\
188 ,
189 },
190 .ReleaseSafe = .{
191 .exclude_os = .{
192 .windows, // TODO
193 },
194 .expect =
195 \\error: AndMyCarIsOutOfGas
196 \\source.zig:2:5: [address] in [function]
197 \\ return error.TheSkyIsFalling;
198 \\ ^
199 \\source.zig:6:5: [address] in [function]
200 \\ return error.AndMyCarIsOutOfGas;
201 \\ ^
202 \\source.zig:11:9: [address] in [function]
203 \\ try bar();
204 \\ ^
205 \\
206 ,
207 },
208 .ReleaseFast = .{
209 .expect =
210 \\error: AndMyCarIsOutOfGas
211 \\
212 ,
213 },
214 .ReleaseSmall = .{
215 .expect =
216 \\error: AndMyCarIsOutOfGas
217 \\
218 ,
219 },
220 });
221
222 cases.addCase(.{
223 .name = "try return from within if-else",
224 .source =
225 \\fn foo() !void {
226 \\ return error.TheSkyIsFalling;
227 \\}
228 \\
229 \\fn bar() !void {
230 \\ return error.AndMyCarIsOutOfGas;
231 \\}
232 \\
233 \\pub fn main() !void {
234 \\ if (foo()) |_| {} else |_| { // error trace should include foo()
235 \\ try bar();
236 \\ }
237 \\}
238 ,
239 .Debug = .{
240 .expect =
241 \\error: AndMyCarIsOutOfGas
242 \\source.zig:2:5: [address] in foo (test)
243 \\ return error.TheSkyIsFalling;
244 \\ ^
245 \\source.zig:6:5: [address] in bar (test)
246 \\ return error.AndMyCarIsOutOfGas;
247 \\ ^
248 \\source.zig:11:9: [address] in main (test)
249 \\ try bar();
250 \\ ^
251 \\
252 ,
253 },
254 .ReleaseSafe = .{
255 .exclude_os = .{
256 .windows, // TODO
257 },
258 .expect =
259 \\error: AndMyCarIsOutOfGas
260 \\source.zig:2:5: [address] in [function]
261 \\ return error.TheSkyIsFalling;
262 \\ ^
263 \\source.zig:6:5: [address] in [function]
264 \\ return error.AndMyCarIsOutOfGas;
265 \\ ^
266 \\source.zig:11:9: [address] in [function]
267 \\ try bar();
268 \\ ^
269 \\
270 ,
271 },
272 .ReleaseFast = .{
273 .expect =
274 \\error: AndMyCarIsOutOfGas
275 \\
276 ,
277 },
278 .ReleaseSmall = .{
279 .expect =
280 \\error: AndMyCarIsOutOfGas
281 \\
282 ,
283 },
284 });
285
101286 cases.addCase(.{
102287 .name = "try try return return",
103288 .source =