authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-03-20 22:30:38+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-04-10 23:17:22+02:00
log998b714708bd7f750a3a076e5afd47c80786e72c
tree27a1aefe9fbcdb4a77675b42c0dcc1e4809cc4ba
parentc3a862522bbb59ee23a53e0562c402283db59b9c

Sema: push to error trace when returning from inline function

There is no reason `inline fn`s should not be subject to error tracing: they are still functions! So, push to the error trace when we return from one, and add a test checking that inline functions do appear in error traces. This also changes how we emit error trace pushes: we no longer duplicate the AIR `ret` instruction in the "error" and "non-error" code paths. I suspect this will lead to slightly better unoptimized codegen, but I may be wrong---I'll take some performance measurements before I merge this.

4 files changed, 144 insertions(+), 66 deletions(-)

src/Sema.zig+90-66
...@@ -18034,28 +18034,24 @@ fn zirRetLoad(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!voi...@@ -18034,28 +18034,24 @@ fn zirRetLoad(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!voi
18034 return sema.analyzeRet(block, operand, src, block.src(.{ .node_offset_return_operand = inst_data.src_node }));18034 return sema.analyzeRet(block, operand, src, block.src(.{ .node_offset_return_operand = inst_data.src_node }));
18035 }18035 }
1803618036
18037 if (sema.wantErrorReturnTracing(sema.fn_ret_ty)) {18037 if (sema.wantErrorReturnTracing()) {
18038 const is_non_err = try sema.analyzePtrIsNonErr(block, src, ret_ptr);18038 const is_non_err = try sema.analyzePtrIsNonErr(block, src, ret_ptr);
18039 return sema.retWithErrTracing(block, src, is_non_err, .ret_load, ret_ptr);18039 try sema.maybePushErrorTrace(block, src, is_non_err);
18040 }18040 }
1804118041
18042 _ = try block.addUnOp(.ret_load, ret_ptr);18042 _ = try block.addUnOp(.ret_load, ret_ptr);
18043}18043}
1804418044
18045fn retWithErrTracing(18045fn maybePushErrorTrace(
18046 sema: *Sema,18046 sema: *Sema,
18047 block: *Block,18047 parent_block: *Block,
18048 src: LazySrcLoc,18048 src: LazySrcLoc,
18049 is_non_err: Air.Inst.Ref,18049 is_non_err: Air.Inst.Ref,
18050 ret_tag: Air.Inst.Tag,
18051 operand: Air.Inst.Ref,
18052) CompileError!void {18050) CompileError!void {
18053 const pt = sema.pt;18051 const pt = sema.pt;
18052
18054 const need_check = switch (is_non_err) {18053 const need_check = switch (is_non_err) {
18055 .bool_true => {18054 .bool_true => return,
18056 _ = try block.addUnOp(ret_tag, operand);
18057 return;
18058 },
18059 .bool_false => false,18055 .bool_false => false,
18060 else => true,18056 else => true,
18061 };18057 };
...@@ -18068,27 +18064,44 @@ fn retWithErrTracing(...@@ -18068,27 +18064,44 @@ fn retWithErrTracing(
18068 const return_err_fn = Air.internedToRef(try sema.getBuiltin(src, .returnError));18064 const return_err_fn = Air.internedToRef(try sema.getBuiltin(src, .returnError));
1806918065
18070 if (!need_check) {18066 if (!need_check) {
18071 try sema.callBuiltin(block, src, return_err_fn, .never_tail, &.{}, .@"error return");18067 try sema.callBuiltin(parent_block, src, return_err_fn, .never_tail, &.{}, .@"error return");
18072 _ = try block.addUnOp(ret_tag, operand);
18073 return;18068 return;
18074 }18069 }
1807518070
18076 var then_block = block.makeSubBlock();18071 var err_block = parent_block.makeSubBlock();
18077 defer then_block.instructions.deinit(gpa);18072 defer err_block.instructions.deinit(gpa);
18078 _ = try then_block.addUnOp(ret_tag, operand);18073 try sema.callBuiltin(&err_block, src, return_err_fn, .never_tail, &.{}, .@"error return");
1807918074
18080 var else_block = block.makeSubBlock();18075 try parent_block.instructions.ensureUnusedCapacity(gpa, 1);
18081 defer else_block.instructions.deinit(gpa);
18082 try sema.callBuiltin(&else_block, src, return_err_fn, .never_tail, &.{}, .@"error return");
18083 _ = try else_block.addUnOp(ret_tag, operand);
1808418076
18085 try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.CondBr).@"struct".fields.len +18077 try sema.air_instructions.ensureUnusedCapacity(gpa, 4);
18086 then_block.instructions.items.len + else_block.instructions.items.len +18078 try sema.air_extra.ensureUnusedCapacity(
18087 @typeInfo(Air.Block).@"struct".fields.len + 1);18079 gpa,
18080 @typeInfo(Air.Block).@"struct".fields.len +
18081 1 + // the main block contains only the `cond_br`
18082 @typeInfo(Air.CondBr).@"struct".fields.len +
18083 1 + // the non-error branch contains only a `br`
18084 err_block.instructions.items.len + 1, // the error branch contains the `returnError` call and a `br`
18085 );
18086
18087 const block_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len);
18088 const cond_br_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len + 1);
18089 const then_br_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len + 2);
18090 const else_br_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len + 3);
18091
18092 const block_payload = sema.addExtraAssumeCapacity(Air.Block{ .body_len = 1 });
18093 sema.air_extra.appendAssumeCapacity(@intFromEnum(cond_br_inst));
18094 sema.air_instructions.appendAssumeCapacity(.{
18095 .tag = .block,
18096 .data = .{ .ty_pl = .{
18097 .ty = .void_type,
18098 .payload = block_payload,
18099 } },
18100 });
1808818101
18089 const cond_br_payload = sema.addExtraAssumeCapacity(Air.CondBr{18102 const cond_br_payload = sema.addExtraAssumeCapacity(Air.CondBr{
18090 .then_body_len = @intCast(then_block.instructions.items.len),18103 .then_body_len = 1,
18091 .else_body_len = @intCast(else_block.instructions.items.len),18104 .else_body_len = @intCast(err_block.instructions.items.len + 1),
18092 .branch_hints = .{18105 .branch_hints = .{
18093 // Weight against error branch.18106 // Weight against error branch.
18094 .true = .likely,18107 .true = .likely,
...@@ -18098,19 +18111,33 @@ fn retWithErrTracing(...@@ -18098,19 +18111,33 @@ fn retWithErrTracing(
18098 .else_cov = .none,18111 .else_cov = .none,
18099 },18112 },
18100 });18113 });
18101 sema.air_extra.appendSliceAssumeCapacity(@ptrCast(then_block.instructions.items));18114 sema.air_extra.appendAssumeCapacity(@intFromEnum(then_br_inst));
18102 sema.air_extra.appendSliceAssumeCapacity(@ptrCast(else_block.instructions.items));18115 sema.air_extra.appendSliceAssumeCapacity(@ptrCast(err_block.instructions.items));
18116 sema.air_extra.appendAssumeCapacity(@intFromEnum(else_br_inst));
18117 sema.air_instructions.appendAssumeCapacity(.{
18118 .tag = .cond_br,
18119 .data = .{ .pl_op = .{
18120 .operand = is_non_err,
18121 .payload = cond_br_payload,
18122 } },
18123 });
1810318124
18104 _ = try block.addInst(.{ .tag = .cond_br, .data = .{ .pl_op = .{18125 const br_inst_data: Air.Inst = .{
18105 .operand = is_non_err,18126 .tag = .br,
18106 .payload = cond_br_payload,18127 .data = .{ .br = .{
18107 } } });18128 .block_inst = block_inst,
18129 .operand = .void_value,
18130 } },
18131 };
18132 sema.air_instructions.appendAssumeCapacity(br_inst_data); // then_br_inst
18133 sema.air_instructions.appendAssumeCapacity(br_inst_data); // else_br_inst
18134
18135 parent_block.instructions.appendAssumeCapacity(block_inst);
18108}18136}
1810918137
18110fn wantErrorReturnTracing(sema: *Sema, fn_ret_ty: Type) bool {18138fn wantErrorReturnTracing(sema: *Sema) bool {
18111 const pt = sema.pt;18139 const zcu = sema.pt.zcu;
18112 const zcu = pt.zcu;18140 return sema.fn_ret_ty.isError(zcu) and zcu.comp.config.any_error_tracing;
18113 return fn_ret_ty.isError(zcu) and zcu.comp.config.any_error_tracing;
18114}18141}
1811518142
18116fn zirSaveErrRetIndex(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {18143fn zirSaveErrRetIndex(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
...@@ -18251,47 +18278,44 @@ fn analyzeRet(...@@ -18251,47 +18278,44 @@ fn analyzeRet(
18251 else => |e| return e,18278 else => |e| return e,
18252 };18279 };
1825318280
18254 if (block.inlining) |inlining| {18281 if (block.isComptime()) {
18282 const inlining = block.inlining orelse {
18283 return sema.fail(block, src, "function called at runtime cannot return value at comptime", .{});
18284 };
18255 assert(!inlining.is_generic_instantiation); // can't `return` in a generic param/ret ty expr18285 assert(!inlining.is_generic_instantiation); // can't `return` in a generic param/ret ty expr
18256 if (block.isComptime()) {18286 const ret_val = try sema.resolveConstValue(block, operand_src, operand, null);
18257 const ret_val = try sema.resolveConstValue(block, operand_src, operand, null);18287 inlining.comptime_result = operand;
18258 inlining.comptime_result = operand;
1825918288
18260 if (sema.fn_ret_ty.isError(zcu) and ret_val.getErrorName(zcu) != .none) {18289 if (sema.fn_ret_ty.isError(zcu) and ret_val.getErrorName(zcu) != .none) {
18261 try sema.comptime_err_ret_trace.append(src);18290 try sema.comptime_err_ret_trace.append(src);
18262 }
18263 return error.ComptimeReturn;
18264 }18291 }
18265 // We are inlining a function call; rewrite the `ret` as a `break`.18292 return error.ComptimeReturn;
18266 const br_inst = try block.addBr(inlining.merges.block_inst, operand);
18267 try inlining.merges.results.append(sema.gpa, operand);
18268 try inlining.merges.br_list.append(sema.gpa, br_inst.toIndex().?);
18269 try inlining.merges.src_locs.append(sema.gpa, operand_src);
18270 return;
18271 } else if (block.isComptime()) {
18272 return sema.fail(block, src, "function called at runtime cannot return value at comptime", .{});
18273 } else if (sema.func_is_naked) {
18274 const msg = msg: {
18275 const msg = try sema.errMsg(src, "cannot return from naked function", .{});
18276 errdefer msg.destroy(sema.gpa);
18277
18278 try sema.errNote(src, msg, "can only return using assembly", .{});
18279 break :msg msg;
18280 };
18281 return sema.failWithOwnedErrorMsg(block, msg);
18282 }18293 }
1828318294
18284 try sema.validateRuntimeValue(block, operand_src, operand);18295 if (block.inlining == null and sema.func_is_naked) return sema.failWithOwnedErrorMsg(block, msg: {
18296 const msg = try sema.errMsg(src, "cannot return from naked function", .{});
18297 errdefer msg.destroy(sema.gpa);
18298
18299 try sema.errNote(src, msg, "can only return using assembly", .{});
18300 break :msg msg;
18301 });
1828518302
18286 const air_tag: Air.Inst.Tag = if (block.wantSafety()) .ret_safe else .ret;18303 if (sema.wantErrorReturnTracing()) {
18287 if (sema.wantErrorReturnTracing(sema.fn_ret_ty)) {
18288 // Avoid adding a frame to the error return trace in case the value is comptime-known
18289 // to be not an error.
18290 const is_non_err = try sema.analyzeIsNonErr(block, operand_src, operand);18304 const is_non_err = try sema.analyzeIsNonErr(block, operand_src, operand);
18291 return sema.retWithErrTracing(block, src, is_non_err, air_tag, operand);18305 try sema.maybePushErrorTrace(block, src, is_non_err);
18292 }18306 }
1829318307
18294 _ = try block.addUnOp(air_tag, operand);18308 if (block.inlining) |inlining| {
18309 assert(!inlining.is_generic_instantiation); // can't `return` in a generic param/ret ty expr
18310 const br_inst = try block.addBr(inlining.merges.block_inst, operand);
18311 try inlining.merges.results.append(sema.gpa, operand);
18312 try inlining.merges.br_list.append(sema.gpa, br_inst.toIndex().?);
18313 try inlining.merges.src_locs.append(sema.gpa, operand_src);
18314 } else {
18315 try sema.validateRuntimeValue(block, operand_src, operand);
18316 const ret_tag: Air.Inst.Tag = if (block.wantSafety()) .ret_safe else .ret;
18317 _ = try block.addUnOp(ret_tag, operand);
18318 }
18295}18319}
1829618320
18297fn floatOpAllowed(tag: Zir.Inst.Tag) bool {18321fn floatOpAllowed(tag: Zir.Inst.Tag) bool {
test/error_traces.zig+50
...@@ -449,4 +449,54 @@ pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext) void {...@@ -449,4 +449,54 @@ pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext) void {
449 .{ .aarch64, .macos },449 .{ .aarch64, .macos },
450 },450 },
451 });451 });
452
453 cases.addCase(.{
454 .name = "trace through inline call",
455 .source =
456 \\pub fn main() !void {
457 \\ try foo();
458 \\}
459 \\inline fn foo() !void {
460 \\ try bar();
461 \\}
462 \\fn bar() !void {
463 \\ return error.ThisIsSoSad;
464 \\}
465 ,
466 .expect_error = "ThisIsSoSad",
467 .expect_trace =
468 \\source.zig:8:5: [address] in bar
469 \\ return error.ThisIsSoSad;
470 \\ ^
471 \\source.zig:5:5: [address] in foo
472 \\ try bar();
473 \\ ^
474 \\source.zig:2:5: [address] in main
475 \\ try foo();
476 \\ ^
477 ,
478 .disable_trace_optimized = &.{
479 .{ .x86_64, .freebsd },
480 .{ .x86_64, .netbsd },
481 .{ .x86_64, .linux },
482 .{ .x86, .linux },
483 .{ .aarch64, .freebsd },
484 .{ .aarch64, .netbsd },
485 .{ .aarch64, .linux },
486 .{ .loongarch64, .linux },
487 .{ .powerpc64le, .linux },
488 .{ .riscv64, .linux },
489 .{ .s390x, .linux },
490 .{ .x86_64, .openbsd },
491 .{ .x86_64, .windows },
492 .{ .x86, .windows },
493 .{ .x86_64, .macos },
494 .{ .aarch64, .macos },
495 },
496 // TODO: the standard library has a bug in PDB parsing where given an address corresponding
497 // to an inline call, the frame we see will be for the *caller*, not the *callee*. As a
498 // result this test gives bogus results on Windows right now.
499 // This is a part of https://codeberg.org/ziglang/zig/issues/30847.
500 .disable_trace_pdb = true,
501 });
452}502}
test/src/ErrorTrace.zig+3
...@@ -17,6 +17,8 @@ pub const Case = struct {...@@ -17,6 +17,8 @@ pub const Case = struct {
17 /// LLVM ReleaseSmall builds always have the trace disabled regardless of this field, because it17 /// LLVM ReleaseSmall builds always have the trace disabled regardless of this field, because it
18 /// seems that LLVM is particularly good at optimizing traces away in those.18 /// seems that LLVM is particularly good at optimizing traces away in those.
19 disable_trace_optimized: []const DisableConfig = &.{},19 disable_trace_optimized: []const DisableConfig = &.{},
20 /// If `true` then we will not test the error trace on Windows due to bugs in PDB handling.
21 disable_trace_pdb: bool = false,
2022
21 pub const DisableConfig = struct { std.Target.Cpu.Arch, std.Target.Os.Tag };23 pub const DisableConfig = struct { std.Target.Cpu.Arch, std.Target.Os.Tag };
22 pub const Backend = enum { llvm, selfhosted };24 pub const Backend = enum { llvm, selfhosted };
...@@ -60,6 +62,7 @@ fn addCaseConfig(...@@ -60,6 +62,7 @@ fn addCaseConfig(
60 const b = self.b;62 const b = self.b;
6163
62 const error_tracing: bool = tracing: {64 const error_tracing: bool = tracing: {
65 if (target.result.os.tag == .windows and case.disable_trace_pdb) break :tracing false;
63 if (optimize == .Debug) break :tracing true;66 if (optimize == .Debug) break :tracing true;
64 if (backend != .llvm) break :tracing true;67 if (backend != .llvm) break :tracing true;
65 if (optimize == .ReleaseSmall) break :tracing false;68 if (optimize == .ReleaseSmall) break :tracing false;
test/src/LlvmIr.zig+1
...@@ -126,6 +126,7 @@ pub fn addCase(self: *LlvmIr, case: TestCase) void {...@@ -126,6 +126,7 @@ pub fn addCase(self: *LlvmIr, case: TestCase) void {
126 .exact => |e| .{ .expected_exact = e },126 .exact => |e| .{ .expected_exact = e },
127 });127 });
128 check.setName(name);128 check.setName(name);
129 check.max_bytes = 64 * 1024 * 1024; // allow fairly big LLVM IR files
129130
130 self.root_step.dependOn(&check.step);131 self.root_step.dependOn(&check.step);
131}132}