From f87156e33c688effcf00b8fa9c2542391423ee78 Mon Sep 17 00:00:00 2001 From: Martin Wickham Date: Wed, 29 Sep 2021 18:37:12 -0500 Subject: [PATCH] Add a panic handler to give better errors for crashes in sema --- lib/std/Thread/Mutex.zig | 177 ++++++------ src/Sema.zig | 6 + src/crash_report.zig | 581 +++++++++++++++++++++++++++++++++++++++ src/main.zig | 6 + src/print_zir.zig | 342 +++++++++++++---------- 5 files changed, 893 insertions(+), 219 deletions(-) create mode 100644 src/crash_report.zig diff --git a/lib/std/Thread/Mutex.zig b/lib/std/Thread/Mutex.zig index a337809a18..7473d9ec7f 100644 --- a/lib/std/Thread/Mutex.zig +++ b/lib/std/Thread/Mutex.zig @@ -33,17 +33,29 @@ const testing = std.testing; const StaticResetEvent = std.thread.StaticResetEvent; /// Try to acquire the mutex without blocking. Returns `null` if the mutex is -/// unavailable. Otherwise returns `Held`. Call `release` on `Held`. -pub fn tryAcquire(m: *Mutex) ?Impl.Held { +/// unavailable. Otherwise returns `Held`. Call `release` on `Held`, or use +/// releaseDirect(). +pub fn tryAcquire(m: *Mutex) ?Held { return m.impl.tryAcquire(); } /// Acquire the mutex. Deadlocks if the mutex is already /// held by the calling thread. -pub fn acquire(m: *Mutex) Impl.Held { +pub fn acquire(m: *Mutex) Held { return m.impl.acquire(); } +/// Release the mutex. Prefer Held.release() if available. +pub fn releaseDirect(m: *Mutex) void { + return m.impl.releaseDirect(); +} + +/// A held mutex handle. Call release to allow other threads to +/// take the mutex. Do not call release() more than once. +/// For more complex scenarios, this handle can be discarded +/// and Mutex.releaseDirect can be called instead. +pub const Held = Impl.Held; + const Impl = if (builtin.single_threaded) Dummy else if (builtin.os.tag == .windows) @@ -53,6 +65,32 @@ else if (std.Thread.use_pthreads) else AtomicMutex; +fn HeldInterface(comptime MutexType: type) type { + return struct { + const Mixin = @This(); + pub const Held = struct { + mutex: *MutexType, + + pub fn release(held: Mixin.Held) void { + held.mutex.releaseDirect(); + } + }; + + pub fn tryAcquire(m: *MutexType) ?Mixin.Held { + if (m.tryAcquireDirect()) { + return Mixin.Held{ .mutex = m }; + } else { + return null; + } + } + + pub fn acquire(m: *MutexType) Mixin.Held { + m.acquireDirect(); + return Mixin.Held{ .mutex = m }; + } + }; +} + pub const AtomicMutex = struct { state: State = .unlocked, @@ -62,39 +100,32 @@ pub const AtomicMutex = struct { waiting, }; - pub const Held = struct { - mutex: *AtomicMutex, + pub usingnamespace HeldInterface(@This()); - pub fn release(held: Held) void { - switch (@atomicRmw(State, &held.mutex.state, .Xchg, .unlocked, .Release)) { - .unlocked => unreachable, - .locked => {}, - .waiting => held.mutex.unlockSlow(), - } - } - }; - - pub fn tryAcquire(m: *AtomicMutex) ?Held { - if (@cmpxchgStrong( + fn tryAcquireDirect(m: *AtomicMutex) bool { + return @cmpxchgStrong( State, &m.state, .unlocked, .locked, .Acquire, .Monotonic, - ) == null) { - return Held{ .mutex = m }; - } else { - return null; - } + ) == null; } - pub fn acquire(m: *AtomicMutex) Held { + fn acquireDirect(m: *AtomicMutex) void { switch (@atomicRmw(State, &m.state, .Xchg, .locked, .Acquire)) { .unlocked => {}, else => |s| m.lockSlow(s), } - return Held{ .mutex = m }; + } + + fn releaseDirect(m: *AtomicMutex) void { + switch (@atomicRmw(State, &m.state, .Xchg, .unlocked, .Release)) { + .unlocked => unreachable, + .locked => {}, + .waiting => m.unlockSlow(), + } } fn lockSlow(m: *AtomicMutex, current_state: State) void { @@ -171,36 +202,20 @@ pub const AtomicMutex = struct { pub const PthreadMutex = struct { pthread_mutex: std.c.pthread_mutex_t = .{}, - pub const Held = struct { - mutex: *PthreadMutex, + pub usingnamespace HeldInterface(@This()); - pub fn release(held: Held) void { - switch (std.c.pthread_mutex_unlock(&held.mutex.pthread_mutex)) { - .SUCCESS => return, - .INVAL => unreachable, - .AGAIN => unreachable, - .PERM => unreachable, - else => unreachable, - } - } - }; - - /// Try to acquire the mutex without blocking. Returns null if - /// the mutex is unavailable. Otherwise returns Held. Call - /// release on Held. - pub fn tryAcquire(m: *PthreadMutex) ?Held { - if (std.c.pthread_mutex_trylock(&m.pthread_mutex) == .SUCCESS) { - return Held{ .mutex = m }; - } else { - return null; - } + /// Try to acquire the mutex without blocking. Returns true if + /// the mutex is unavailable. Otherwise returns false. Call + /// release when done. + fn tryAcquireDirect(m: *PthreadMutex) bool { + return std.c.pthread_mutex_trylock(&m.pthread_mutex) == .SUCCESS; } /// Acquire the mutex. Will deadlock if the mutex is already /// held by the calling thread. - pub fn acquire(m: *PthreadMutex) Held { + fn acquireDirect(m: *PthreadMutex) void { switch (std.c.pthread_mutex_lock(&m.pthread_mutex)) { - .SUCCESS => return Held{ .mutex = m }, + .SUCCESS => {}, .INVAL => unreachable, .BUSY => unreachable, .AGAIN => unreachable, @@ -209,6 +224,16 @@ pub const PthreadMutex = struct { else => unreachable, } } + + fn releaseDirect(m: *PthreadMutex) void { + switch (std.c.pthread_mutex_unlock(&m.pthread_mutex)) { + .SUCCESS => return, + .INVAL => unreachable, + .AGAIN => unreachable, + .PERM => unreachable, + else => unreachable, + } + } }; /// This has the sematics as `Mutex`, however it does not actually do any @@ -216,58 +241,50 @@ pub const PthreadMutex = struct { pub const Dummy = struct { lock: @TypeOf(lock_init) = lock_init, + pub usingnamespace HeldInterface(@This()); + const lock_init = if (std.debug.runtime_safety) false else {}; - pub const Held = struct { - mutex: *Dummy, - - pub fn release(held: Held) void { - if (std.debug.runtime_safety) { - held.mutex.lock = false; - } - } - }; - - /// Try to acquire the mutex without blocking. Returns null if - /// the mutex is unavailable. Otherwise returns Held. Call - /// release on Held. - pub fn tryAcquire(m: *Dummy) ?Held { + /// Try to acquire the mutex without blocking. Returns false if + /// the mutex is unavailable. Otherwise returns true. + fn tryAcquireDirect(m: *Dummy) bool { if (std.debug.runtime_safety) { - if (m.lock) return null; + if (m.lock) return false; m.lock = true; } - return Held{ .mutex = m }; + return true; } /// Acquire the mutex. Will deadlock if the mutex is already /// held by the calling thread. - pub fn acquire(m: *Dummy) Held { - return m.tryAcquire() orelse @panic("deadlock detected"); + fn acquireDirect(m: *Dummy) void { + if (!m.tryAcquireDirect()) { + @panic("deadlock detected"); + } + } + + fn releaseDirect(m: *Dummy) void { + if (std.debug.runtime_safety) { + m.lock = false; + } } }; const WindowsMutex = struct { srwlock: windows.SRWLOCK = windows.SRWLOCK_INIT, - pub const Held = struct { - mutex: *WindowsMutex, + pub usingnamespace HeldInterface(@This()); - pub fn release(held: Held) void { - windows.kernel32.ReleaseSRWLockExclusive(&held.mutex.srwlock); - } - }; - - pub fn tryAcquire(m: *WindowsMutex) ?Held { - if (windows.kernel32.TryAcquireSRWLockExclusive(&m.srwlock) != windows.FALSE) { - return Held{ .mutex = m }; - } else { - return null; - } + fn tryAcquireDirect(m: *WindowsMutex) bool { + return windows.kernel32.TryAcquireSRWLockExclusive(&m.srwlock) != windows.FALSE; } - pub fn acquire(m: *WindowsMutex) Held { + fn acquireDirect(m: *WindowsMutex) void { windows.kernel32.AcquireSRWLockExclusive(&m.srwlock); - return Held{ .mutex = m }; + } + + fn releaseDirect(m: *WindowsMutex) void { + windows.kernel32.ReleaseSRWLockExclusive(&m.srwlock); } }; diff --git a/src/Sema.zig b/src/Sema.zig index b71c4f1847..f9f0160064 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -90,6 +90,7 @@ const LazySrcLoc = Module.LazySrcLoc; const RangeSet = @import("RangeSet.zig"); const target_util = @import("target.zig"); const Package = @import("Package.zig"); +const crash_report = @import("crash_report.zig"); pub const InstMap = std.AutoHashMapUnmanaged(Zir.Inst.Index, Air.Inst.Ref); @@ -153,11 +154,16 @@ pub fn analyzeBody( var orig_captures: usize = parent_capture_scope.captures.count(); + var crash_info = crash_report.prepAnalyzeBody(sema, block, body); + crash_info.push(); + defer crash_info.pop(); + // We use a while(true) loop here to avoid a redundant way of breaking out of // the loop. The only way to break out of the loop is with a `noreturn` // instruction. var i: usize = 0; const result = while (true) { + crash_info.setBodyIndex(i); const inst = body[i]; const air_inst: Air.Inst.Ref = switch (tags[inst]) { // zig fmt: off diff --git a/src/crash_report.zig b/src/crash_report.zig new file mode 100644 index 0000000000..84f4b8db84 --- /dev/null +++ b/src/crash_report.zig @@ -0,0 +1,581 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const debug = std.debug; +const os = std.os; +const io = std.io; +const print_zir = @import("print_zir.zig"); + +const Module = @import("Module.zig"); +const Sema = @import("Sema.zig"); +const Zir = @import("Zir.zig"); + +pub const is_enabled = builtin.mode == .Debug; + +/// To use these crash report diagnostics, publish these symbols in your main file. +/// You will also need to call initialize() on startup, preferably as the very first operation in your program. +pub const root_decls = struct { + pub const panic = if (is_enabled) compilerPanic else std.builtin.default_panic; + pub const enable_segfault_handler = if (is_enabled) false else debug.default_enable_segfault_handler; +}; + +/// Install signal handlers to identify crashes and report diagnostics. +pub fn initialize() void { + if (is_enabled and debug.have_segfault_handling_support) { + attachSegfaultHandler(); + } +} + +fn En(comptime T: type) type { + return if (is_enabled) T else void; +} + +fn en(val: anytype) En(@TypeOf(val)) { + return if (is_enabled) val else {}; +} + +pub const AnalyzeBody = struct { + parent: if (is_enabled) ?*AnalyzeBody else void, + sema: En(*Sema), + block: En(*Module.Scope.Block), + body: En([]const Zir.Inst.Index), + body_index: En(usize), + + pub fn push(self: *@This()) void { + if (!is_enabled) return; + const head = &zir_state; + debug.assert(self.parent == null); + self.parent = head.*; + head.* = self; + } + + pub fn pop(self: *@This()) void { + if (!is_enabled) return; + const head = &zir_state; + const old = head.*.?; + debug.assert(old == self); + head.* = old.parent; + } + + pub fn setBodyIndex(self: *@This(), index: usize) void { + if (!is_enabled) return; + self.body_index = index; + } +}; + +threadlocal var zir_state: ?*AnalyzeBody = if (is_enabled) null else @compileError("Cannot use zir_state if crash_report is disabled."); + +pub fn prepAnalyzeBody(sema: *Sema, block: *Module.Scope.Block, body: []const Zir.Inst.Index) AnalyzeBody { + if (is_enabled) { + return .{ + .parent = null, + .sema = sema, + .block = block, + .body = body, + .body_index = 0, + }; + } else { + if (@sizeOf(AnalyzeBody) != 0) + @compileError("AnalyzeBody must have zero size when crash reports are disabled"); + return undefined; + } +} + +fn dumpStatusReport() !void { + const anal = zir_state orelse return; + // Note: We have the panic mutex here, so we can safely use the global crash heap. + var fba = std.heap.FixedBufferAllocator.init(&crash_heap); + const allocator = &fba.allocator; + + const stderr = io.getStdErr().writer(); + const block: *Scope.Block = anal.block; + + try stderr.writeAll("Analyzing "); + try writeFullyQualifiedDeclWithFile(block.src_decl, stderr); + try stderr.writeAll("\n"); + + print_zir.renderInstructionContext( + allocator, + anal.body, + anal.body_index, + block.src_decl.getFileScope(), + block.src_decl.src_node, + 6, // indent + stderr, + ) catch |err| switch (err) { + error.OutOfMemory => try stderr.writeAll(" \n"), + else => |e| return e, + }; + try stderr.writeAll(" For full context, use the command\n zig ast-check -t "); + try writeFilePath(block.src_decl.getFileScope(), stderr); + try stderr.writeAll("\n\n"); + + var parent = anal.parent; + while (parent) |curr| { + fba.reset(); + try stderr.writeAll(" in "); + try writeFullyQualifiedDeclWithFile(curr.block.src_decl, stderr); + try stderr.writeAll("\n > "); + print_zir.renderSingleInstruction( + allocator, + curr.body[curr.body_index], + curr.block.src_decl.getFileScope(), + curr.block.src_decl.src_node, + 6, // indent + stderr, + ) catch |err| switch (err) { + error.OutOfMemory => try stderr.writeAll(" \n"), + else => |e| return e, + }; + try stderr.writeAll("\n"); + + parent = curr.parent; + } + + try stderr.writeAll("\n"); +} + +const Scope = Module.Scope; +const Decl = Module.Decl; + +var crash_heap: [16 * 4096]u8 = undefined; + +fn writeFilePath(file: *Scope.File, stream: anytype) !void { + if (file.pkg.root_src_directory.path) |path| { + try stream.writeAll(path); + try stream.writeAll(std.fs.path.sep_str); + } + try stream.writeAll(file.sub_file_path); +} + +fn writeFullyQualifiedDeclWithFile(decl: *Decl, stream: anytype) !void { + try writeFilePath(decl.getFileScope(), stream); + try stream.writeAll(": "); + try decl.namespace.renderFullyQualifiedName(std.mem.sliceTo(decl.name, 0), stream); +} + +fn compilerPanic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace) noreturn { + PanicSwitch.preDispatch(); + @setCold(true); + const ret_addr = @returnAddress(); + const stack_ctx: StackContext = .{ .current = .{ .ret_addr = ret_addr } }; + PanicSwitch.dispatch(error_return_trace, stack_ctx, msg); +} + +/// Attaches a global SIGSEGV handler +pub fn attachSegfaultHandler() void { + if (!debug.have_segfault_handling_support) { + @compileError("segfault handler not supported for this target"); + } + if (builtin.os.tag == .windows) { + _ = os.windows.kernel32.AddVectoredExceptionHandler(0, handleSegfaultWindows); + return; + } + var act = os.Sigaction{ + .handler = .{ .sigaction = handleSegfaultLinux }, + .mask = os.empty_sigset, + .flags = (os.SA.SIGINFO | os.SA.RESTART | os.SA.RESETHAND), + }; + + os.sigaction(os.SIG.SEGV, &act, null); + os.sigaction(os.SIG.ILL, &act, null); + os.sigaction(os.SIG.BUS, &act, null); +} + +fn handleSegfaultLinux(sig: i32, info: *const os.siginfo_t, ctx_ptr: ?*const c_void) callconv(.C) noreturn { + // TODO: use alarm() here to prevent infinite loops + PanicSwitch.preDispatch(); + + const addr = switch (builtin.os.tag) { + .linux => @ptrToInt(info.fields.sigfault.addr), + .freebsd => @ptrToInt(info.addr), + .netbsd => @ptrToInt(info.info.reason.fault.addr), + .openbsd => @ptrToInt(info.data.fault.addr), + .solaris => @ptrToInt(info.reason.fault.addr), + else => @compileError("TODO implement handleSegfaultLinux for new linux OS"), + }; + + var err_buffer: [128]u8 = undefined; + const error_msg = switch (sig) { + os.SIG.SEGV => std.fmt.bufPrint(&err_buffer, "Segmentation fault at address 0x{x}", .{addr}) catch "Segmentation fault", + os.SIG.ILL => std.fmt.bufPrint(&err_buffer, "Illegal instruction at address 0x{x}", .{addr}) catch "Illegal instruction", + os.SIG.BUS => std.fmt.bufPrint(&err_buffer, "Bus error at address 0x{x}", .{addr}) catch "Bus error", + else => std.fmt.bufPrint(&err_buffer, "Unknown error (signal {}) at address 0x{x}", .{ sig, addr }) catch "Unknown error", + }; + + const stack_ctx: StackContext = switch (builtin.cpu.arch) { + .i386 => ctx: { + const ctx = @ptrCast(*const os.ucontext_t, @alignCast(@alignOf(os.ucontext_t), ctx_ptr)); + const ip = @intCast(usize, ctx.mcontext.gregs[os.REG.EIP]); + const bp = @intCast(usize, ctx.mcontext.gregs[os.REG.EBP]); + break :ctx StackContext{ .exception = .{ .bp = bp, .ip = ip } }; + }, + .x86_64 => ctx: { + const ctx = @ptrCast(*const os.ucontext_t, @alignCast(@alignOf(os.ucontext_t), ctx_ptr)); + const ip = switch (builtin.os.tag) { + .linux, .netbsd, .solaris => @intCast(usize, ctx.mcontext.gregs[os.REG.RIP]), + .freebsd => @intCast(usize, ctx.mcontext.rip), + .openbsd => @intCast(usize, ctx.sc_rip), + else => unreachable, + }; + const bp = switch (builtin.os.tag) { + .linux, .netbsd, .solaris => @intCast(usize, ctx.mcontext.gregs[os.REG.RBP]), + .openbsd => @intCast(usize, ctx.sc_rbp), + .freebsd => @intCast(usize, ctx.mcontext.rbp), + else => unreachable, + }; + break :ctx StackContext{ .exception = .{ .bp = bp, .ip = ip } }; + }, + .arm => ctx: { + const ctx = @ptrCast(*const os.ucontext_t, @alignCast(@alignOf(os.ucontext_t), ctx_ptr)); + const ip = @intCast(usize, ctx.mcontext.arm_pc); + const bp = @intCast(usize, ctx.mcontext.arm_fp); + break :ctx StackContext{ .exception = .{ .bp = bp, .ip = ip } }; + }, + .aarch64 => ctx: { + const ctx = @ptrCast(*const os.ucontext_t, @alignCast(@alignOf(os.ucontext_t), ctx_ptr)); + const ip = @intCast(usize, ctx.mcontext.pc); + // x29 is the ABI-designated frame pointer + const bp = @intCast(usize, ctx.mcontext.regs[29]); + break :ctx StackContext{ .exception = .{ .bp = bp, .ip = ip } }; + }, + else => .not_supported, + }; + + PanicSwitch.dispatch(null, stack_ctx, error_msg); +} + +const WindowsSegfaultMessage = union(enum) { + literal: []const u8, + segfault: void, + illegal_instruction: void, +}; + +fn handleSegfaultWindows(info: *os.windows.EXCEPTION_POINTERS) callconv(os.windows.WINAPI) c_long { + switch (info.ExceptionRecord.ExceptionCode) { + os.windows.EXCEPTION_DATATYPE_MISALIGNMENT => handleSegfaultWindowsExtra(info, .{ .literal = "Unaligned Memory Access" }), + os.windows.EXCEPTION_ACCESS_VIOLATION => handleSegfaultWindowsExtra(info, .segfault), + os.windows.EXCEPTION_ILLEGAL_INSTRUCTION => handleSegfaultWindowsExtra(info, .illegal_instruction), + os.windows.EXCEPTION_STACK_OVERFLOW => handleSegfaultWindowsExtra(info, .{ .literal = "Stack Overflow" }), + else => return os.windows.EXCEPTION_CONTINUE_SEARCH, + } +} + +fn handleSegfaultWindowsExtra(info: *os.windows.EXCEPTION_POINTERS, comptime msg: WindowsSegfaultMessage) noreturn { + PanicSwitch.preDispatch(); + + const stack_ctx = if (@hasDecl(os.windows, "CONTEXT")) ctx: { + const regs = info.ContextRecord.getRegs(); + break :ctx StackContext{ .exception = .{ .bp = regs.bp, .ip = regs.ip } }; + } else ctx: { + const addr = @ptrToInt(info.ExceptionRecord.ExceptionAddress); + break :ctx StackContext{ .current = .{ .ret_addr = addr } }; + }; + + switch (msg) { + .literal => |err| PanicSwitch.dispatch(null, stack_ctx, err), + .segfault => { + const format_item = "Segmentation fault at address 0x{x}"; + var buf: [format_item.len + 32]u8 = undefined; // 32 is arbitrary, but sufficiently large + const to_print = std.fmt.bufPrint(&buf, format_item, .{info.ExceptionRecord.ExceptionInformation[1]}) catch unreachable; + PanicSwitch.dispatch(null, stack_ctx, to_print); + }, + .illegal_instruction => { + const ip: ?usize = switch (stack_ctx) { + .exception => |ex| ex.ip, + .current => |cur| cur.ret_addr, + .not_supported => null, + }; + + if (ip) |addr| { + const format_item = "Illegal instruction at address 0x{x}"; + var buf: [format_item.len + 32]u8 = undefined; // 32 is arbitrary, but sufficiently large + const to_print = std.fmt.bufPrint(&buf, format_item, .{addr}) catch unreachable; + PanicSwitch.dispatch(null, stack_ctx, to_print); + } else { + PanicSwitch.dispatch(null, stack_ctx, "Illegal Instruction"); + } + }, + } +} + +const StackContext = union(enum) { + current: struct { + ret_addr: ?usize, + }, + exception: struct { + bp: usize, + ip: usize, + }, + not_supported: void, + + pub fn dumpStackTrace(ctx: @This()) void { + switch (ctx) { + .current => |ct| { + debug.dumpCurrentStackTrace(ct.ret_addr); + }, + .exception => |ex| { + debug.dumpStackTraceFromBase(ex.bp, ex.ip); + }, + .not_supported => { + const stderr = io.getStdErr().writer(); + stderr.writeAll("Stack trace not supported on this platform.\n") catch {}; + }, + } + } +}; + +const PanicSwitch = struct { + const RecoverStage = enum { + initialize, + report_stack, + release_mutex, + release_ref_count, + abort, + silent_abort, + }; + + const RecoverVerbosity = enum { + message_and_stack, + message_only, + silent, + }; + + const PanicState = struct { + recover_stage: RecoverStage = .initialize, + recover_verbosity: RecoverVerbosity = .message_and_stack, + panic_ctx: StackContext = undefined, + panic_trace: ?*const std.builtin.StackTrace = null, + awaiting_dispatch: bool = false, + }; + + /// Counter for the number of threads currently panicking. + /// Updated atomically before taking the panic_mutex. + /// In recoverable cases, the program will not abort + /// until all panicking threads have dumped their traces. + var panicking: u8 = 0; + + // Locked to avoid interleaving panic messages from multiple threads. + var panic_mutex = std.Thread.Mutex{}; + + /// Tracks the state of the current panic. If the code within the + /// panic triggers a secondary panic, this allows us to recover. + threadlocal var panic_state_raw: PanicState = .{}; + + /// The segfault handlers above need to do some work before they can dispatch + /// this switch. Calling preDispatch() first makes that work fault tolerant. + pub fn preDispatch() void { + // TODO: We want segfaults to trigger the panic recursively here, + // but if there is a segfault accessing this TLS slot it will cause an + // infinite loop. We should use `alarm()` to prevent the infinite + // loop and maybe also use a non-thread-local global to detect if + // it's happening and print a message. + var panic_state: *volatile PanicState = &panic_state_raw; + if (panic_state.awaiting_dispatch) { + dispatch(null, .{ .current = .{ .ret_addr = null } }, "Panic while preparing callstack"); + } + panic_state.awaiting_dispatch = true; + } + + /// This is the entry point to a panic-tolerant panic handler. + /// preDispatch() *MUST* be called exactly once before calling this. + /// A threadlocal "recover_stage" is updated throughout the process. + /// If a panic happens during the panic, the recover_stage will be + /// used to select a recover* function to call to resume the panic. + /// The recover_verbosity field is used to handle panics while reporting + /// panics within panics. If the panic handler triggers a panic, it will + /// attempt to log an additional stack trace for the secondary panic. If + /// that panics, it will fall back to just logging the panic message. If + /// it can't even do that witout panicing, it will recover without logging + /// anything about the internal panic. Depending on the state, "recover" + /// here may just mean "call abort". + pub fn dispatch( + trace: ?*const std.builtin.StackTrace, + stack_ctx: StackContext, + msg: []const u8, + ) noreturn { + var panic_state: *volatile PanicState = &panic_state_raw; + debug.assert(panic_state.awaiting_dispatch); + panic_state.awaiting_dispatch = false; + nosuspend switch (panic_state.recover_stage) { + .initialize => goTo(initPanic, .{ panic_state, trace, stack_ctx, msg }), + .report_stack => goTo(recoverReportStack, .{ panic_state, trace, stack_ctx, msg }), + .release_mutex => goTo(recoverReleaseMutex, .{ panic_state, trace, stack_ctx, msg }), + .release_ref_count => goTo(recoverReleaseRefCount, .{ panic_state, trace, stack_ctx, msg }), + .abort => goTo(recoverAbort, .{ panic_state, trace, stack_ctx, msg }), + .silent_abort => goTo(abort, .{}), + }; + } + + noinline fn initPanic( + state: *volatile PanicState, + trace: ?*const std.builtin.StackTrace, + stack: StackContext, + msg: []const u8, + ) noreturn { + // use a temporary so there's only one volatile store + const new_state = PanicState{ + .recover_stage = .abort, + .panic_ctx = stack, + .panic_trace = trace, + }; + state.* = new_state; + + _ = @atomicRmw(u8, &panicking, .Add, 1, .SeqCst); + + state.recover_stage = .release_ref_count; + + _ = panic_mutex.acquire(); + + state.recover_stage = .release_mutex; + + const stderr = io.getStdErr().writer(); + if (builtin.single_threaded) { + stderr.print("panic: ", .{}) catch goTo(releaseMutex, .{state}); + } else { + const current_thread_id = std.Thread.getCurrentId(); + stderr.print("thread {} panic: ", .{current_thread_id}) catch goTo(releaseMutex, .{state}); + } + stderr.print("{s}\n", .{msg}) catch goTo(releaseMutex, .{state}); + + state.recover_stage = .report_stack; + + dumpStatusReport() catch |err| { + stderr.print("\nIntercepted error.{} while dumping current state. Continuing...\n", .{err}) catch {}; + }; + + goTo(reportStack, .{state}); + } + + noinline fn recoverReportStack( + state: *volatile PanicState, + trace: ?*const std.builtin.StackTrace, + stack: StackContext, + msg: []const u8, + ) noreturn { + recover(state, trace, stack, msg); + + state.recover_stage = .release_mutex; + const stderr = io.getStdErr().writer(); + stderr.writeAll("\nOriginal Error:\n") catch {}; + goTo(reportStack, .{state}); + } + + noinline fn reportStack(state: *volatile PanicState) noreturn { + state.recover_stage = .release_mutex; + + if (state.panic_trace) |t| { + debug.dumpStackTrace(t.*); + } + state.panic_ctx.dumpStackTrace(); + + goTo(releaseMutex, .{state}); + } + + noinline fn recoverReleaseMutex( + state: *volatile PanicState, + trace: ?*const std.builtin.StackTrace, + stack: StackContext, + msg: []const u8, + ) noreturn { + recover(state, trace, stack, msg); + goTo(releaseMutex, .{state}); + } + + noinline fn releaseMutex(state: *volatile PanicState) noreturn { + state.recover_stage = .abort; + + panic_mutex.releaseDirect(); + + goTo(releaseRefCount, .{state}); + } + + noinline fn recoverReleaseRefCount( + state: *volatile PanicState, + trace: ?*const std.builtin.StackTrace, + stack: StackContext, + msg: []const u8, + ) noreturn { + recover(state, trace, stack, msg); + goTo(releaseRefCount, .{state}); + } + + noinline fn releaseRefCount(state: *volatile PanicState) noreturn { + state.recover_stage = .abort; + + if (@atomicRmw(u8, &panicking, .Sub, 1, .SeqCst) != 1) { + // Another thread is panicking, wait for the last one to finish + // and call abort() + + // Sleep forever without hammering the CPU + var event: std.Thread.StaticResetEvent = .{}; + event.wait(); + // This should be unreachable, recurse into recoverAbort. + @panic("event.wait() returned"); + } + + goTo(abort, .{}); + } + + noinline fn recoverAbort( + state: *volatile PanicState, + trace: ?*const std.builtin.StackTrace, + stack: StackContext, + msg: []const u8, + ) noreturn { + recover(state, trace, stack, msg); + + state.recover_stage = .silent_abort; + const stderr = io.getStdErr().writer(); + stderr.writeAll("Aborting...\n") catch {}; + goTo(abort, .{}); + } + + noinline fn abort() noreturn { + os.abort(); + } + + inline fn goTo(comptime func: anytype, args: anytype) noreturn { + // TODO: Tailcall is broken right now, but eventually this should be used + // to avoid blowing up the stack. It's ok for now though, there are no + // cycles in the state machine so the max stack usage is bounded. + //@call(.{.modifier = .always_tail}, func, args); + @call(.{}, func, args); + } + + fn recover( + state: *volatile PanicState, + trace: ?*const std.builtin.StackTrace, + stack: StackContext, + msg: []const u8, + ) void { + switch (state.recover_verbosity) { + .message_and_stack => { + // lower the verbosity, and restore it at the end if we don't panic. + state.recover_verbosity = .message_only; + + const stderr = io.getStdErr().writer(); + stderr.writeAll("\nPanicked during a panic: ") catch {}; + stderr.writeAll(msg) catch {}; + stderr.writeAll("\nInner panic stack:\n") catch {}; + if (trace) |t| { + debug.dumpStackTrace(t.*); + } + stack.dumpStackTrace(); + + state.recover_verbosity = .message_and_stack; + }, + .message_only => { + state.recover_verbosity = .silent; + + const stderr = io.getStdErr().writer(); + stderr.writeAll("\nPanicked while dumping inner panic stack: ") catch {}; + stderr.writeAll(msg) catch {}; + stderr.writeAll("\n") catch {}; + + // If we succeed, restore all the way to dumping the stack. + state.recover_verbosity = .message_and_stack; + }, + .silent => {}, + } + } +}; diff --git a/src/main.zig b/src/main.zig index 213b9506e7..e0be4b6021 100644 --- a/src/main.zig +++ b/src/main.zig @@ -20,6 +20,10 @@ const translate_c = @import("translate_c.zig"); const Cache = @import("Cache.zig"); const target_util = @import("target.zig"); const ThreadPool = @import("ThreadPool.zig"); +const crash_report = @import("crash_report.zig"); + +// Crash report needs to override the panic handler and other root decls +pub usingnamespace crash_report.root_decls; pub fn fatal(comptime format: []const u8, args: anytype) noreturn { std.log.emerg(format, args); @@ -134,6 +138,8 @@ var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{ }){}; pub fn main() anyerror!void { + crash_report.initialize(); + var gpa_need_deinit = false; const gpa = gpa: { if (!std.builtin.link_libc) { diff --git a/src/print_zir.zig b/src/print_zir.zig index 54fd7e632f..1d97f792e6 100644 --- a/src/print_zir.zig +++ b/src/print_zir.zig @@ -24,15 +24,20 @@ pub fn renderAsTextToFile( .code = scope_file.zir, .indent = 0, .parent_decl_node = 0, + .recurse_decls = true, + .recurse_blocks = true, }; + var raw_stream = std.io.bufferedWriter(fs_file.writer()); + const stream = raw_stream.writer(); + const main_struct_inst = Zir.main_struct_inst; - try fs_file.writer().print("%{d} ", .{main_struct_inst}); - try writer.writeInstToStream(fs_file.writer(), main_struct_inst); - try fs_file.writeAll("\n"); + try stream.print("%{d} ", .{main_struct_inst}); + try writer.writeInstToStream(stream, main_struct_inst); + try stream.writeAll("\n"); const imports_index = scope_file.zir.extra[@enumToInt(Zir.ExtraIndex.imports)]; if (imports_index != 0) { - try fs_file.writeAll("Imports:\n"); + try stream.writeAll("Imports:\n"); const extra = scope_file.zir.extraData(Zir.Inst.Imports, imports_index); var import_i: u32 = 0; @@ -44,13 +49,74 @@ pub fn renderAsTextToFile( const src: LazySrcLoc = .{ .token_abs = item.data.token }; const import_path = scope_file.zir.nullTerminatedString(item.data.name); - try fs_file.writer().print(" @import(\"{}\") ", .{ + try stream.print(" @import(\"{}\") ", .{ std.zig.fmtEscapes(import_path), }); - try writer.writeSrc(fs_file.writer(), src); - try fs_file.writer().writeAll("\n"); + try writer.writeSrc(stream, src); + try stream.writeAll("\n"); } } + + try raw_stream.flush(); +} + +pub fn renderInstructionContext( + gpa: *Allocator, + block: []const Zir.Inst.Index, + block_index: usize, + scope_file: *Module.Scope.File, + parent_decl_node: Ast.Node.Index, + indent: u32, + stream: anytype, +) !void { + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + + var writer: Writer = .{ + .gpa = gpa, + .arena = &arena.allocator, + .file = scope_file, + .code = scope_file.zir, + .indent = if (indent < 2) 2 else indent, + .parent_decl_node = parent_decl_node, + .recurse_decls = false, + .recurse_blocks = true, + }; + + try writer.writeBody(stream, block[0..block_index]); + try stream.writeByteNTimes(' ', writer.indent - 2); + try stream.print("> %{d} ", .{block[block_index]}); + try writer.writeInstToStream(stream, block[block_index]); + try stream.writeByte('\n'); + if (block_index + 1 < block.len) { + try writer.writeBody(stream, block[block_index + 1 ..]); + } +} + +pub fn renderSingleInstruction( + gpa: *Allocator, + inst: Zir.Inst.Index, + scope_file: *Module.Scope.File, + parent_decl_node: Ast.Node.Index, + indent: u32, + stream: anytype, +) !void { + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + + var writer: Writer = .{ + .gpa = gpa, + .arena = &arena.allocator, + .file = scope_file, + .code = scope_file.zir, + .indent = indent, + .parent_decl_node = parent_decl_node, + .recurse_decls = false, + .recurse_blocks = false, + }; + + try stream.print("%{d} ", .{inst}); + try writer.writeInstToStream(stream, inst); } const Writer = struct { @@ -59,7 +125,9 @@ const Writer = struct { file: *Module.Scope.File, code: Zir, indent: u32, - parent_decl_node: u32, + parent_decl_node: Ast.Node.Index, + recurse_decls: bool, + recurse_blocks: bool, fn relativeToNodeIndex(self: *Writer, offset: i32) Ast.Node.Index { return @bitCast(Ast.Node.Index, offset + @bitCast(i32, self.parent_decl_node)); @@ -567,12 +635,8 @@ const Writer = struct { try stream.print("\"{}\", ", .{ std.zig.fmtEscapes(self.code.nullTerminatedString(extra.data.name)), }); - try stream.writeAll("{\n"); - self.indent += 2; - try self.writeBody(stream, body); - self.indent -= 2; - try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}) "); + try self.writeBracedBody(stream, body); + try stream.writeAll(") "); try self.writeSrc(stream, inst_data.src()); } @@ -881,12 +945,8 @@ const Writer = struct { const inst_data = self.code.instructions.items(.data)[inst].pl_node; const extra = self.code.extraData(Zir.Inst.Block, inst_data.payload_index); const body = self.code.extra[extra.end..][0..extra.data.body_len]; - try stream.writeAll("{\n"); - self.indent += 2; - try self.writeBody(stream, body); - self.indent -= 2; - try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}) "); + try self.writeBracedBody(stream, body); + try stream.writeAll(") "); } fn writePlNodeCondBr(self: *Writer, stream: anytype, inst: Zir.Inst.Index) !void { @@ -895,17 +955,11 @@ const Writer = struct { const then_body = self.code.extra[extra.end..][0..extra.data.then_body_len]; const else_body = self.code.extra[extra.end + then_body.len ..][0..extra.data.else_body_len]; try self.writeInstRef(stream, extra.data.condition); - try stream.writeAll(", {\n"); - self.indent += 2; - try self.writeBody(stream, then_body); - self.indent -= 2; - try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}, {\n"); - self.indent += 2; - try self.writeBody(stream, else_body); - self.indent -= 2; - try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}) "); + try stream.writeAll(", "); + try self.writeBracedBody(stream, then_body); + try stream.writeAll(", "); + try self.writeBracedBody(stream, else_body); + try stream.writeAll(") "); try self.writeSrc(stream, inst_data.src()); } @@ -963,17 +1017,10 @@ const Writer = struct { } else { const prev_parent_decl_node = self.parent_decl_node; if (src_node) |off| self.parent_decl_node = self.relativeToNodeIndex(off); + try self.writeBracedDecl(stream, body); + try stream.writeAll(", {\n"); + self.indent += 2; - if (body.len == 0) { - try stream.writeAll("{}, {\n"); - } else { - try stream.writeAll("{\n"); - try self.writeBody(stream, body); - - try stream.writeByteNTimes(' ', self.indent - 2); - try stream.writeAll("}, {\n"); - } - const bits_per_field = 4; const fields_per_u32 = 32 / bits_per_field; const bit_bags_count = std.math.divCeil(usize, fields_len, fields_per_u32) catch unreachable; @@ -1096,17 +1143,10 @@ const Writer = struct { const prev_parent_decl_node = self.parent_decl_node; if (src_node) |off| self.parent_decl_node = self.relativeToNodeIndex(off); + try self.writeBracedDecl(stream, body); + try stream.writeAll(", {\n"); + self.indent += 2; - if (body.len == 0) { - try stream.writeAll("{}, {\n"); - } else { - try stream.writeAll("{\n"); - try self.writeBody(stream, body); - - try stream.writeByteNTimes(' ', self.indent - 2); - try stream.writeAll("}, {\n"); - } - const bits_per_field = 4; const fields_per_u32 = 32 / bits_per_field; const bit_bags_count = std.math.divCeil(usize, fields_len, fields_per_u32) catch unreachable; @@ -1251,18 +1291,25 @@ const Writer = struct { try stream.writeAll(")"); } } - const tag = self.code.instructions.items(.tag)[decl_index]; - try stream.print(" line({d}) hash({}): %{d} = {s}(", .{ - line, std.fmt.fmtSliceHexLower(&hash_bytes), decl_index, @tagName(tag), - }); - const decl_block_inst_data = self.code.instructions.items(.data)[decl_index].pl_node; - const sub_decl_node_off = decl_block_inst_data.src_node; - self.parent_decl_node = self.relativeToNodeIndex(sub_decl_node_off); - try self.writePlNodeBlockWithoutSrc(stream, decl_index); - self.parent_decl_node = parent_decl_node; - try self.writeSrc(stream, decl_block_inst_data.src()); - try stream.writeAll("\n"); + if (self.recurse_decls) { + const tag = self.code.instructions.items(.tag)[decl_index]; + try stream.print(" line({d}) hash({}): %{d} = {s}(", .{ + line, std.fmt.fmtSliceHexLower(&hash_bytes), decl_index, @tagName(tag), + }); + + const decl_block_inst_data = self.code.instructions.items(.data)[decl_index].pl_node; + const sub_decl_node_off = decl_block_inst_data.src_node; + self.parent_decl_node = self.relativeToNodeIndex(sub_decl_node_off); + try self.writePlNodeBlockWithoutSrc(stream, decl_index); + self.parent_decl_node = parent_decl_node; + try self.writeSrc(stream, decl_block_inst_data.src()); + try stream.writeAll("\n"); + } else { + try stream.print(" line({d}) hash({}): %{d} = ...\n", .{ + line, std.fmt.fmtSliceHexLower(&hash_bytes), decl_index, + }); + } } return extra_index; } @@ -1329,17 +1376,10 @@ const Writer = struct { } else { const prev_parent_decl_node = self.parent_decl_node; if (src_node) |off| self.parent_decl_node = self.relativeToNodeIndex(off); + try self.writeBracedDecl(stream, body); + try stream.writeAll(", {\n"); + self.indent += 2; - if (body.len == 0) { - try stream.writeAll("{}, {\n"); - } else { - try stream.writeAll("{\n"); - try self.writeBody(stream, body); - - try stream.writeByteNTimes(' ', self.indent - 2); - try stream.writeAll("}, {\n"); - } - const bit_bags_count = std.math.divCeil(usize, fields_len, 32) catch unreachable; const body_end = extra_index; extra_index += bit_bags_count; @@ -1463,18 +1503,18 @@ const Writer = struct { try self.writeInstRef(stream, extra.data.operand); + self.indent += 2; + if (special.body.len != 0) { const prong_name = switch (special_prong) { .@"else" => "else", .under => "_", else => unreachable, }; - try stream.print(", {s} => {{\n", .{prong_name}); - self.indent += 2; - try self.writeBody(stream, special.body); - self.indent -= 2; + try stream.writeAll(",\n"); try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}"); + try stream.print("{s} => ", .{prong_name}); + try self.writeBracedBody(stream, special.body); } var extra_index: usize = special.end; @@ -1488,16 +1528,16 @@ const Writer = struct { const body = self.code.extra[extra_index..][0..body_len]; extra_index += body_len; - try stream.writeAll(", "); - try self.writeInstRef(stream, item_ref); - try stream.writeAll(" => {\n"); - self.indent += 2; - try self.writeBody(stream, body); - self.indent -= 2; + try stream.writeAll(",\n"); try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}"); + try self.writeInstRef(stream, item_ref); + try stream.writeAll(" => "); + try self.writeBracedBody(stream, body); } } + + self.indent -= 2; + try stream.writeAll(") "); try self.writeSrc(stream, inst_data.src()); } @@ -1527,18 +1567,18 @@ const Writer = struct { try self.writeInstRef(stream, extra.data.operand); + self.indent += 2; + if (special.body.len != 0) { const prong_name = switch (special_prong) { .@"else" => "else", .under => "_", else => unreachable, }; - try stream.print(", {s} => {{\n", .{prong_name}); - self.indent += 2; - try self.writeBody(stream, special.body); - self.indent -= 2; + try stream.writeAll(",\n"); try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}"); + try stream.print("{s} => ", .{prong_name}); + try self.writeBracedBody(stream, special.body); } var extra_index: usize = special.end; @@ -1552,14 +1592,11 @@ const Writer = struct { const body = self.code.extra[extra_index..][0..body_len]; extra_index += body_len; - try stream.writeAll(", "); - try self.writeInstRef(stream, item_ref); - try stream.writeAll(" => {\n"); - self.indent += 2; - try self.writeBody(stream, body); - self.indent -= 2; + try stream.writeAll(",\n"); try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}"); + try self.writeInstRef(stream, item_ref); + try stream.writeAll(" => "); + try self.writeBracedBody(stream, body); } } { @@ -1574,8 +1611,11 @@ const Writer = struct { const items = self.code.refSlice(extra_index, items_len); extra_index += items_len; - for (items) |item_ref| { - try stream.writeAll(", "); + try stream.writeAll(",\n"); + try stream.writeByteNTimes(' ', self.indent); + + for (items) |item_ref, item_i| { + if (item_i != 0) try stream.writeAll(", "); try self.writeInstRef(stream, item_ref); } @@ -1586,7 +1626,9 @@ const Writer = struct { const item_last = @intToEnum(Zir.Inst.Ref, self.code.extra[extra_index]); extra_index += 1; - try stream.writeAll(", "); + if (range_i != 0 or items.len != 0) { + try stream.writeAll(", "); + } try self.writeInstRef(stream, item_first); try stream.writeAll("..."); try self.writeInstRef(stream, item_last); @@ -1594,14 +1636,13 @@ const Writer = struct { const body = self.code.extra[extra_index..][0..body_len]; extra_index += body_len; - try stream.writeAll(" => {\n"); - self.indent += 2; - try self.writeBody(stream, body); - self.indent -= 2; - try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}"); + try stream.writeAll(" => "); + try self.writeBracedBody(stream, body); } } + + self.indent -= 2; + try stream.writeAll(") "); try self.writeSrc(stream, inst_data.src()); } @@ -1796,12 +1837,8 @@ const Writer = struct { const extra = self.code.extraData(Zir.Inst.Block, inst_data.payload_index); const body = self.code.extra[extra.end..][0..extra.data.body_len]; try self.writeInstRef(stream, inst_data.lhs); - try stream.writeAll(", {\n"); - self.indent += 2; - try self.writeBody(stream, body); - self.indent -= 2; - try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("})"); + try stream.writeAll(", "); + try self.writeBracedBody(stream, body); } fn writeIntType(self: *Writer, stream: anytype, inst: Zir.Inst.Index) !void { @@ -1846,12 +1883,8 @@ const Writer = struct { if (ret_ty_body.len == 0) { try stream.writeAll("ret_ty=void"); } else { - try stream.writeAll("ret_ty={\n"); - self.indent += 2; - try self.writeBody(stream, ret_ty_body); - self.indent -= 2; - try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}"); + try stream.writeAll("ret_ty="); + try self.writeBracedBody(stream, ret_ty_body); } try self.writeOptionalInstRef(stream, ", cc=", cc); @@ -1860,16 +1893,9 @@ const Writer = struct { try self.writeFlag(stream, ", extern", is_extern); try self.writeFlag(stream, ", inferror", inferred_error_set); - if (body.len == 0) { - try stream.writeAll(", body={}) "); - } else { - try stream.writeAll(", body={\n"); - self.indent += 2; - try self.writeBody(stream, body); - self.indent -= 2; - try stream.writeByteNTimes(' ', self.indent); - try stream.writeAll("}) "); - } + try stream.writeAll(", body="); + try self.writeBracedBody(stream, body); + try stream.writeAll(") "); if (body.len != 0) { try stream.print("(lbrace={d}:{d},rbrace={d}:{d}) ", .{ src_locs.lbrace_line, @truncate(u16, src_locs.columns), @@ -1929,18 +1955,19 @@ const Writer = struct { } fn writeSrc(self: *Writer, stream: anytype, src: LazySrcLoc) !void { - const tree = self.file.tree; - const src_loc: Module.SrcLoc = .{ - .file_scope = self.file, - .parent_decl_node = self.parent_decl_node, - .lazy = src, - }; - // Caller must ensure AST tree is loaded. - const abs_byte_off = src_loc.byteOffset(self.gpa) catch unreachable; - const delta_line = std.zig.findLineColumn(tree.source, abs_byte_off); - try stream.print("{s}:{d}:{d}", .{ - @tagName(src), delta_line.line + 1, delta_line.column + 1, - }); + if (self.file.tree_loaded) { + const tree = self.file.tree; + const src_loc: Module.SrcLoc = .{ + .file_scope = self.file, + .parent_decl_node = self.parent_decl_node, + .lazy = src, + }; + const abs_byte_off = src_loc.byteOffset(self.gpa) catch unreachable; + const delta_line = std.zig.findLineColumn(tree.source, abs_byte_off); + try stream.print("{s}:{d}:{d}", .{ + @tagName(src), delta_line.line + 1, delta_line.column + 1, + }); + } } fn writeSrcNode(self: *Writer, stream: anytype, src_node: ?i32) !void { @@ -1950,6 +1977,43 @@ const Writer = struct { return self.writeSrc(stream, src); } + fn writeBracedDecl(self: *Writer, stream: anytype, body: []const Zir.Inst.Index) !void { + try self.writeBracedBodyConditional(stream, body, self.recurse_decls); + } + + fn writeBracedBody(self: *Writer, stream: anytype, body: []const Zir.Inst.Index) !void { + try self.writeBracedBodyConditional(stream, body, self.recurse_blocks); + } + + fn writeBracedBodyConditional(self: *Writer, stream: anytype, body: []const Zir.Inst.Index, enabled: bool) !void { + if (body.len == 0) { + try stream.writeAll("{}"); + } else if (enabled) { + try stream.writeAll("{\n"); + self.indent += 2; + try self.writeBody(stream, body); + self.indent -= 2; + try stream.writeByteNTimes(' ', self.indent); + try stream.writeAll("}"); + } else if (body.len == 1) { + try stream.writeByte('{'); + try self.writeInstIndex(stream, body[0]); + try stream.writeByte('}'); + } else if (body.len == 2) { + try stream.writeByte('{'); + try self.writeInstIndex(stream, body[0]); + try stream.writeAll(", "); + try self.writeInstIndex(stream, body[1]); + try stream.writeByte('}'); + } else { + try stream.writeByte('{'); + try self.writeInstIndex(stream, body[0]); + try stream.writeAll(".."); + try self.writeInstIndex(stream, body[body.len - 1]); + try stream.writeByte('}'); + } + } + fn writeBody(self: *Writer, stream: anytype, body: []const Zir.Inst.Index) !void { for (body) |inst| { try stream.writeByteNTimes(' ', self.indent);