authorgravatar for mason@gamesbymason.comMason Remaley <mason@gamesbymason.com> 2026-04-09 15:43:05-07:00
committergravatar for mason@gamesbymason.comMason Remaley <mason@gamesbymason.com> 2026-04-12 04:01:29-07:00
logc2cbb944ba377db141e3dc5a890d955932accea2
tree8bf3852032934f69c0a50ca97dbf465d7aa7c8d2
parent6bf583c4baf6b33166176f03e7f570bbd4607a8b

Further improvements to stack trace type


2 files changed, 67 insertions(+), 32 deletions(-)

lib/std/debug.zig+62-27
...@@ -609,8 +609,33 @@ fn waitForOtherThreadToFinishPanicking() void {...@@ -609,8 +609,33 @@ fn waitForOtherThreadToFinishPanicking() void {
609/// This data structure is used by the Zig language code generation and609/// This data structure is used by the Zig language code generation and
610/// therefore must be kept in sync with the compiler implementation.610/// therefore must be kept in sync with the compiler implementation.
611pub const StackTrace = struct {611pub const StackTrace = struct {
612 index: usize,612 /// Each element is the "return address" of a function call, meaning the instruction address
613 /// which control flow will return to when the function returns.
614 ///
615 /// The first slice element corresponds to the innermost stack frame, and the last element to
616 /// the outermost.
617 ///
618 /// Inlined function calls do not have meaningful return addresses and are therefore not
619 /// included in this slice. Instead, when printing the stack trace, the source locations of
620 /// inline calls should be read from debug information and the corresponding "inline frames"
621 /// printed in the appropriate locations.
613 return_addresses: []usize,622 return_addresses: []usize,
623 /// Indicates whether any stack frames were omitted from `return_addresses`.
624 skipped: SkippedAddresses,
625
626};
627
628/// Indicates how many addresses were skipped in a trace.
629pub const SkippedAddresses = enum(usize) {
630 /// No addresses were omitted: `return_addresses` contains all stack frames, including the
631 /// outermost.
632 none = 0,
633 /// It is not known whether any frames were omitted.
634 unknown = std.math.maxInt(usize),
635 /// The full stack trace was available, but some frames are not included in
636 /// `return_addresses` due to buffer size limitations. The enum value is the exact number of
637 /// addresses which were omitted.
638 _,
614};639};
615640
616pub const StackUnwindOptions = struct {641pub const StackUnwindOptions = struct {
...@@ -633,8 +658,8 @@ pub const StackUnwindOptions = struct {...@@ -633,8 +658,8 @@ pub const StackUnwindOptions = struct {
633/// See `writeCurrentStackTrace` to immediately print the trace instead of capturing it.658/// See `writeCurrentStackTrace` to immediately print the trace instead of capturing it.
634pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) StackTrace {659pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) StackTrace {
635 const empty_trace: StackTrace = .{660 const empty_trace: StackTrace = .{
636 .index = 0,
637 .return_addresses = &.{},661 .return_addresses = &.{},
662 .skipped = .none,
638 };663 };
639 if (!std.options.allow_stack_tracing) return empty_trace;664 if (!std.options.allow_stack_tracing) return empty_trace;
640 var it: StackIterator = .init(options.context);665 var it: StackIterator = .init(options.context);
...@@ -646,17 +671,17 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf:...@@ -646,17 +671,17 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf:
646 var total_frames: usize = 0;671 var total_frames: usize = 0;
647 var index: usize = 0;672 var index: usize = 0;
648 var wait_for = options.first_address;673 var wait_for = options.first_address;
649 // Ideally, we would iterate the whole stack so that the `index` in the returned trace was674 // Ideally, we would iterate the whole stack so that the `index - min(buf.len, index)` would be
650 // indicative of how many frames were skipped. However, this has a significant runtime cost675 // indicative of how many frames were skipped. However, this has a significant runtime cost
651 // in some cases, so at least for now, we don't do that.676 // in some cases, so at least for now, we don't do that.
652 while (index < addr_buf.len) switch (it.next(io)) {677 const skipped: SkippedAddresses = while (index < addr_buf.len) switch (it.next(io)) {
653 .switch_to_fp => if (!it.stratOk(options.allow_unsafe_unwind)) break,678 .switch_to_fp => if (!it.stratOk(options.allow_unsafe_unwind)) break .unknown,
654 .end => break,679 .end => break .none,
655 .frame => |ret_addr| {680 .frame => |ret_addr| {
656 if (total_frames > 10_000) {681 if (total_frames > 10_000) {
657 // Limit the number of frames in case of (e.g.) broken debug information which is682 // Limit the number of frames in case of (e.g.) broken debug information which is
658 // getting unwinding stuck in a loop.683 // getting unwinding stuck in a loop.
659 break;684 break .unknown;
660 }685 }
661 total_frames += 1;686 total_frames += 1;
662 if (wait_for) |target| {687 if (wait_for) |target| {
...@@ -666,10 +691,10 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf:...@@ -666,10 +691,10 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf:
666 addr_buf[index] = ret_addr;691 addr_buf[index] = ret_addr;
667 index += 1;692 index += 1;
668 },693 },
669 };694 } else .unknown;
670 return .{695 return .{
671 .index = index,
672 .return_addresses = addr_buf[0..index],696 .return_addresses = addr_buf[0..index],
697 .skipped = skipped,
673 };698 };
674}699}
675/// Write the current stack trace to `writer`, annotated with source locations.700/// Write the current stack trace to `writer`, annotated with source locations.
...@@ -792,19 +817,21 @@ pub const FormatStackTrace = struct {...@@ -792,19 +817,21 @@ pub const FormatStackTrace = struct {
792817
793/// Write a previously captured error return trace to `writer`, annotated with source locations.818/// Write a previously captured error return trace to `writer`, annotated with source locations.
794pub fn writeErrorReturnTrace(et: *const std.builtin.ErrorReturnTrace, t: Io.Terminal) Writer.Error!void {819pub fn writeErrorReturnTrace(et: *const std.builtin.ErrorReturnTrace, t: Io.Terminal) Writer.Error!void {
795 // Fetch `et.index` straight away. Aside from avoiding redundant loads, this prevents issues if820 // We take the slice by value, preventing the length from being mutated if an error occurs while
796 // errors are encountered while writing the stack trace.821 // writing the stack trace.
797 try writeTrace(et.instruction_addresses, et.index, t, false);822 const len = @min(et.instruction_addresses.len, et.index);
823 const skipped = et.index - len;
824 try writeTrace(et.instruction_addresses[0..len], @enumFromInt(skipped), t, false);
798}825}
799826
800/// Write a previously captured stack trace to `writer`, annotated with source locations.827/// Write a previously captured stack trace to `writer`, annotated with source locations.
801pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void {828pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void {
802 try writeTrace(st.return_addresses, st.index, t, true);829 try writeTrace(st.return_addresses, st.skipped, t, true);
803}830}
804831
805fn writeTrace(832fn writeTrace(
806 addresses: []const usize,833 addresses: []const usize,
807 n_frames: usize,834 skipped: SkippedAddresses,
808 t: Io.Terminal,835 t: Io.Terminal,
809 resolve_inline_callers: bool,836 resolve_inline_callers: bool,
810) Writer.Error!void {837) Writer.Error!void {
...@@ -816,7 +843,7 @@ fn writeTrace(...@@ -816,7 +843,7 @@ fn writeTrace(
816 return;843 return;
817 }844 }
818845
819 if (n_frames == 0) return writer.writeAll("(empty stack trace)\n");846 if (addresses.len == 0) return writer.writeAll("(empty stack trace)\n");
820 const di = getSelfDebugInfo() catch |err| switch (err) {847 const di = getSelfDebugInfo() catch |err| switch (err) {
821 error.UnsupportedTarget => {848 error.UnsupportedTarget => {
822 t.setColor(.dim) catch {};849 t.setColor(.dim) catch {};
...@@ -826,19 +853,26 @@ fn writeTrace(...@@ -826,19 +853,26 @@ fn writeTrace(
826 },853 },
827 };854 };
828 const io = std.Options.debug_io;855 const io = std.Options.debug_io;
829 const captured_frames = @min(n_frames, addresses.len);856 for (addresses) |addr| {
830 for (addresses[0..captured_frames]) |ret_addr| {857 // `addr` is the return address, which is *after* the function call.
831 // `ret_addr` is the return address, which is *after* the function call.
832 // Subtract 1 to get an address *in* the function call for a better source location.858 // Subtract 1 to get an address *in* the function call for a better source location.
833 try printSourceAtAddress(io, di, t, .{859 try printSourceAtAddress(io, di, t, .{
834 .address = ret_addr -| StackIterator.ra_call_offset,860 .address = addr -| StackIterator.ra_call_offset,
835 .resolve_inline_callers = resolve_inline_callers,861 .resolve_inline_callers = resolve_inline_callers,
836 });862 });
837 }863 }
838 if (n_frames > captured_frames) {864 switch (skipped) {
839 t.setColor(.bold) catch {};865 .none => {},
840 try writer.print("({d} additional stack frames skipped...)\n", .{n_frames - captured_frames});866 .unknown => {
841 t.setColor(.reset) catch {};867 t.setColor(.bold) catch {};
868 try writer.writeAll("(additional stack frames may have been skipped...)\n");
869 t.setColor(.reset) catch {};
870 },
871 else => |n| {
872 t.setColor(.bold) catch {};
873 try writer.print("({d} additional stack frames skipped due to buffer size limitations...)\n", .{n});
874 t.setColor(.reset) catch {};
875 },
842 }876 }
843}877}
844/// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors.878/// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors.
...@@ -1712,8 +1746,8 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize...@@ -1712,8 +1746,8 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize
1712 t.notes[t.index] = note;1746 t.notes[t.index] = note;
1713 const addrs = &t.addrs[t.index];1747 const addrs = &t.addrs[t.index];
1714 const st = captureCurrentStackTrace(.{ .first_address = addr }, addrs);1748 const st = captureCurrentStackTrace(.{ .first_address = addr }, addrs);
1715 if (st.index < addrs.len) {1749 if (st.return_addresses.len < addrs.len) {
1716 @memset(addrs[st.index..], 0); // zero unused frames to indicate end of trace1750 @memset(addrs[st.return_addresses.len..], 0); // zero unused frames to indicate end of trace
1717 }1751 }
1718 }1752 }
1719 // Keep counting even if the end is reached so that the1753 // Keep counting even if the end is reached so that the
...@@ -1731,9 +1765,10 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize...@@ -1731,9 +1765,10 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize
1731 stderr.writer.print("{s}:\n", .{t.notes[i]}) catch return;1765 stderr.writer.print("{s}:\n", .{t.notes[i]}) catch return;
1732 var frames_array_mutable = frames_array;1766 var frames_array_mutable = frames_array;
1733 const frames = mem.sliceTo(frames_array_mutable[0..], 0);1767 const frames = mem.sliceTo(frames_array_mutable[0..], 0);
1768 const len = @min(t.index, frames.len);
1734 const stack_trace: StackTrace = .{1769 const stack_trace: StackTrace = .{
1735 .index = frames.len,1770 .return_addresses = frames[0..len],
1736 .return_addresses = frames,1771 .skipped = if (len < frames.len) .none else .unknown,
1737 };1772 };
1738 writeStackTrace(&stack_trace, stderr) catch return;1773 writeStackTrace(&stack_trace, stderr) catch return;
1739 }1774 }
lib/std/heap/debug_allocator.zig+5-5
...@@ -237,8 +237,8 @@ pub fn DebugAllocator(comptime config: Config) type {...@@ -237,8 +237,8 @@ pub fn DebugAllocator(comptime config: Config) type {
237 len += 1;237 len += 1;
238 }238 }
239 return .{239 return .{
240 .return_addresses = stack_addresses,240 .return_addresses = stack_addresses[0..len],
241 .index = len,241 .skipped = if (len < stack_addresses.len) .none else .unknown,
242 };242 };
243 }243 }
244244
...@@ -339,8 +339,8 @@ pub fn DebugAllocator(comptime config: Config) type {...@@ -339,8 +339,8 @@ pub fn DebugAllocator(comptime config: Config) type {
339 len += 1;339 len += 1;
340 }340 }
341 return .{341 return .{
342 .return_addresses = stack_addresses,342 .return_addresses = stack_addresses[0..len],
343 .index = len,343 .skipped = if (len < stack_addresses.len) .none else .unknown,
344 };344 };
345 }345 }
346346
...@@ -508,7 +508,7 @@ pub fn DebugAllocator(comptime config: Config) type {...@@ -508,7 +508,7 @@ pub fn DebugAllocator(comptime config: Config) type {
508508
509 fn collectStackTrace(first_trace_addr: usize, addr_buf: *[stack_n]usize) void {509 fn collectStackTrace(first_trace_addr: usize, addr_buf: *[stack_n]usize) void {
510 const st = std.debug.captureCurrentStackTrace(.{ .first_address = first_trace_addr }, addr_buf);510 const st = std.debug.captureCurrentStackTrace(.{ .first_address = first_trace_addr }, addr_buf);
511 @memset(addr_buf[@min(st.index, addr_buf.len)..], 0);511 @memset(addr_buf[@min(st.return_addresses.len, addr_buf.len)..], 0);
512 }512 }
513513
514 fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void {514 fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void {