mutex: Io.Mutex,
/// Accessed through `Module.Adapter`.
modules: std.ArrayHashMapUnmanaged(Module, void, Module.Context, false),

pub const init: SelfInfo = .{
    .mutex = .init,
    .modules = .empty,
};
pub fn deinit(si: *SelfInfo, io: Io) void {
    _ = io;
    const gpa = std.debug.getDebugInfoAllocator();
    for (si.modules.keys()) |*module| {
        unwind: {
            const u = &(module.unwind orelse break :unwind catch break :unwind);
            if (u.dwarf) |*dwarf| dwarf.deinit(gpa);
        }
        file: {
            const f = &(module.file orelse break :file catch break :file);
            f.deinit(gpa);
        }
    }
    si.modules.deinit(gpa);
}

pub fn getSymbols(
    si: *SelfInfo,
    io: Io,
    symbol_allocator: Allocator,
    text_arena: Allocator,
    address: usize,
    resolve_inline_callers: bool,
    symbols: *std.ArrayList(std.debug.Symbol),
) Error!void {
    _ = resolve_inline_callers;
    const gpa = std.debug.getDebugInfoAllocator();

    const module = try si.findModule(gpa, io, address);
    defer si.mutex.unlock(io);

    const file = try module.getFile(gpa, io);

    // This is not necessarily the same as the vmaddr_slide that dyld would report. This is
    // because the segments in the file on disk might differ from the ones in memory. Normally
    // we wouldn't necessarily expect that to work, but /usr/lib/dyld is incredibly annoying:
    // it exists on disk (necessarily, because the kernel needs to load it!), but is also in
    // the dyld cache (dyld actually restart itself from cache after loading it), and the two
    // versions have (very) different segment base addresses. It's sort of like a large slide
    // has been applied to all addresses in memory. For an optimal experience, we consider the
    // on-disk vmaddr instead of the in-memory one.
    const vaddr_offset = module.text_base - file.text_vmaddr;

    const vaddr = address - vaddr_offset;

    const ofile_dwarf, const ofile_vaddr = file.getDwarfForAddress(gpa, io, vaddr) catch {
        // Return at least the symbol name if available.
        return symbols.append(symbol_allocator, .{
            .name = try file.lookupSymbolName(vaddr),
            .compile_unit_name = null,
            .source_location = null,
        });
    };

    const compile_unit = ofile_dwarf.findCompileUnit(native_endian, ofile_vaddr) catch {
        // Return at least the symbol name if available.
        return symbols.append(symbol_allocator, .{
            .name = try file.lookupSymbolName(vaddr),
            .compile_unit_name = null,
            .source_location = null,
        });
    };

    try symbols.append(symbol_allocator, .{
        .name = ofile_dwarf.getSymbolName(ofile_vaddr) orelse
            try file.lookupSymbolName(vaddr),
        .compile_unit_name = compile_unit.die.getAttrString(
            ofile_dwarf,
            native_endian,
            std.dwarf.AT.name,
            ofile_dwarf.section(.debug_str),
            compile_unit,
        ) catch |err| switch (err) {
            error.MissingDebugInfo, error.InvalidDebugInfo => null,
        },
        .source_location = ofile_dwarf.getLineNumberInfo(
            gpa,
            text_arena,
            native_endian,
            compile_unit,
            ofile_vaddr,
        ) catch null,
    });
}
pub fn getModuleName(si: *SelfInfo, io: Io, address: usize) Error![]const u8 {
    _ = si;
    _ = io;
    // This function is marked as deprecated; however, it is significantly more
    // performant than `dladdr` (since the latter also does a very slow symbol
    // lookup), so let's use it since it's still available.
    return std.mem.span(std.c.dyld_image_path_containing_address(
        @ptrFromInt(address),
    ) orelse return error.MissingDebugInfo);
}
pub fn getModuleSlide(si: *SelfInfo, io: Io, address: usize) Error!usize {
    const gpa = std.debug.getDebugInfoAllocator();
    const module = try si.findModule(gpa, io, address);
    defer si.mutex.unlock(io);
    const header: *std.macho.mach_header_64 = @ptrFromInt(module.text_base);
    const raw_macho: [*]u8 = @ptrCast(header);
    var it = macho.LoadCommandIterator.init(header, raw_macho[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds]) catch unreachable;
    const text_vmaddr = while (it.next() catch unreachable) |load_cmd| {
        if (load_cmd.hdr.cmd != .SEGMENT_64) continue;
        const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
        if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
        break segment_cmd.vmaddr;
    } else unreachable;
    return module.text_base - text_vmaddr;
}

pub const can_unwind: bool = true;
pub const UnwindContext = std.debug.Dwarf.SelfUnwinder;
/// Unwind a frame using MachO compact unwind info (from `__unwind_info`).
/// If the compact encoding can't encode a way to unwind a frame, it will
/// defer unwinding to DWARF, in which case `__eh_frame` will be used if available.
pub fn unwindFrame(si: *SelfInfo, io: Io, context: *UnwindContext) Error!usize {
    return unwindFrameInner(si, io, context) catch |err| switch (err) {
        error.InvalidDebugInfo,
        error.MissingDebugInfo,
        error.UnsupportedDebugInfo,
        error.ReadFailed,
        error.OutOfMemory,
        error.Unexpected,
        error.Canceled,
        => |e| return e,

        error.UnsupportedRegister,
        error.UnsupportedAddrSize,
        error.UnimplementedUserOpcode,
        => return error.UnsupportedDebugInfo,

        error.Overflow,
        error.EndOfStream,
        error.StreamTooLong,
        error.InvalidOpcode,
        error.InvalidOperation,
        error.InvalidOperand,
        error.InvalidRegister,
        error.IncompatibleRegisterSize,
        => return error.InvalidDebugInfo,
    };
}
fn unwindFrameInner(si: *SelfInfo, io: Io, context: *UnwindContext) !usize {
    const gpa = std.debug.getDebugInfoAllocator();
    const module = try si.findModule(gpa, io, context.pc);
    defer si.mutex.unlock(io);

    const unwind: *Module.Unwind = try module.getUnwindInfo(gpa);

    const ip_reg_num = comptime Dwarf.ipRegNum(builtin.target.cpu.arch).?;
    const fp_reg_num = comptime Dwarf.fpRegNum(builtin.target.cpu.arch);
    const sp_reg_num = comptime Dwarf.spRegNum(builtin.target.cpu.arch);

    const unwind_info = unwind.unwind_info orelse return error.MissingDebugInfo;
    if (unwind_info.len < @sizeOf(macho.unwind_info_section_header)) return error.InvalidDebugInfo;
    const header: *align(1) const macho.unwind_info_section_header = @ptrCast(unwind_info);

    const index_byte_count = header.indexCount * @sizeOf(macho.unwind_info_section_header_index_entry);
    if (unwind_info.len < header.indexSectionOffset + index_byte_count) return error.InvalidDebugInfo;
    const indices: []align(1) const macho.unwind_info_section_header_index_entry = @ptrCast(unwind_info[header.indexSectionOffset..][0..index_byte_count]);
    if (indices.len == 0) return error.MissingDebugInfo;

    // offset of the PC into the `__TEXT` segment
    const pc_text_offset = context.pc - module.text_base;

    const start_offset: u32, const first_level_offset: u32 = index: {
        var left: usize = 0;
        var len: usize = indices.len;
        while (len > 1) {
            const mid = left + len / 2;
            if (pc_text_offset < indices[mid].functionOffset) {
                len /= 2;
            } else {
                left = mid;
                len -= len / 2;
            }
        }
        break :index .{ indices[left].secondLevelPagesSectionOffset, indices[left].functionOffset };
    };
    // An offset of 0 is a sentinel indicating a range does not have unwind info.
    if (start_offset == 0) return error.MissingDebugInfo;

    const common_encodings_byte_count = header.commonEncodingsArrayCount * @sizeOf(macho.compact_unwind_encoding_t);
    if (unwind_info.len < header.commonEncodingsArraySectionOffset + common_encodings_byte_count) return error.InvalidDebugInfo;
    const common_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
        unwind_info[header.commonEncodingsArraySectionOffset..][0..common_encodings_byte_count],
    );

    if (unwind_info.len < start_offset + @sizeOf(macho.UNWIND_SECOND_LEVEL)) return error.InvalidDebugInfo;
    const kind: *align(1) const macho.UNWIND_SECOND_LEVEL = @ptrCast(unwind_info[start_offset..]);

    const entry: struct {
        function_offset: usize,
        raw_encoding: u32,
    } = switch (kind.*) {
        .REGULAR => entry: {
            if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_regular_second_level_page_header)) return error.InvalidDebugInfo;
            const page_header: *align(1) const macho.unwind_info_regular_second_level_page_header = @ptrCast(unwind_info[start_offset..]);

            const entries_byte_count = page_header.entryCount * @sizeOf(macho.unwind_info_regular_second_level_entry);
            if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
            const entries: []align(1) const macho.unwind_info_regular_second_level_entry = @ptrCast(
                unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
            );
            if (entries.len == 0) return error.InvalidDebugInfo;

            var left: usize = 0;
            var len: usize = entries.len;
            while (len > 1) {
                const mid = left + len / 2;
                if (pc_text_offset < entries[mid].functionOffset) {
                    len /= 2;
                } else {
                    left = mid;
                    len -= len / 2;
                }
            }
            break :entry .{
                .function_offset = entries[left].functionOffset,
                .raw_encoding = entries[left].encoding,
            };
        },
        .COMPRESSED => entry: {
            if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_compressed_second_level_page_header)) return error.InvalidDebugInfo;
            const page_header: *align(1) const macho.unwind_info_compressed_second_level_page_header = @ptrCast(unwind_info[start_offset..]);

            const entries_byte_count = page_header.entryCount * @sizeOf(macho.UnwindInfoCompressedEntry);
            if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
            const entries: []align(1) const macho.UnwindInfoCompressedEntry = @ptrCast(
                unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
            );
            if (entries.len == 0) return error.InvalidDebugInfo;

            var left: usize = 0;
            var len: usize = entries.len;
            while (len > 1) {
                const mid = left + len / 2;
                if (pc_text_offset < first_level_offset + entries[mid].funcOffset) {
                    len /= 2;
                } else {
                    left = mid;
                    len -= len / 2;
                }
            }
            const entry = entries[left];

            const function_offset = first_level_offset + entry.funcOffset;
            if (entry.encodingIndex < common_encodings.len) {
                break :entry .{
                    .function_offset = function_offset,
                    .raw_encoding = common_encodings[entry.encodingIndex],
                };
            }

            const local_index = entry.encodingIndex - common_encodings.len;
            const local_encodings_byte_count = page_header.encodingsCount * @sizeOf(macho.compact_unwind_encoding_t);
            if (unwind_info.len < start_offset + page_header.encodingsPageOffset + local_encodings_byte_count) return error.InvalidDebugInfo;
            const local_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
                unwind_info[start_offset + page_header.encodingsPageOffset ..][0..local_encodings_byte_count],
            );
            if (local_index >= local_encodings.len) return error.InvalidDebugInfo;
            break :entry .{
                .function_offset = function_offset,
                .raw_encoding = local_encodings[local_index],
            };
        },
        else => return error.InvalidDebugInfo,
    };

    if (entry.raw_encoding == 0) return error.MissingDebugInfo;

    const encoding: macho.CompactUnwindEncoding = @bitCast(entry.raw_encoding);
    const new_ip = switch (builtin.cpu.arch) {
        .x86_64 => switch (encoding.mode.x86_64) {
            .OLD => return error.UnsupportedDebugInfo,
            .RBP_FRAME => ip: {
                const frame = encoding.value.x86_64.frame;

                const fp = (try dwarfRegNative(&context.cpu_state, fp_reg_num)).*;
                const new_sp = fp + 2 * @sizeOf(usize);

                const ip_ptr = fp + @sizeOf(usize);
                const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
                const new_fp = @as(*const usize, @ptrFromInt(fp)).*;

                (try dwarfRegNative(&context.cpu_state, fp_reg_num)).* = new_fp;
                (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
                (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;

                const regs: [5]u3 = .{
                    frame.reg0,
                    frame.reg1,
                    frame.reg2,
                    frame.reg3,
                    frame.reg4,
                };
                for (regs, 0..) |reg, i| {
                    if (reg == 0) continue;
                    const addr = fp - frame.frame_offset * @sizeOf(usize) + i * @sizeOf(usize);
                    const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(reg);
                    (try dwarfRegNative(&context.cpu_state, reg_number)).* = @as(*const usize, @ptrFromInt(addr)).*;
                }

                break :ip new_ip;
            },
            .STACK_IMMD,
            .STACK_IND,
            => ip: {
                const frameless = encoding.value.x86_64.frameless;

                const sp = (try dwarfRegNative(&context.cpu_state, sp_reg_num)).*;
                const stack_size: usize = stack_size: {
                    if (encoding.mode.x86_64 == .STACK_IMMD) {
                        break :stack_size @as(usize, frameless.stack.direct.stack_size) * @sizeOf(usize);
                    }
                    // In .STACK_IND, the stack size is inferred from the subq instruction at the beginning of the function.
                    const sub_offset_addr =
                        module.text_base +
                        entry.function_offset +
                        frameless.stack.indirect.sub_offset;
                    // `sub_offset_addr` points to the offset of the literal within the instruction
                    const sub_operand = @as(*align(1) const u32, @ptrFromInt(sub_offset_addr)).*;
                    break :stack_size sub_operand + @sizeOf(usize) * @as(usize, frameless.stack.indirect.stack_adjust);
                };

                // Decode the Lehmer-coded sequence of registers.
                // For a description of the encoding see lib/libc/include/any-macos.13-any/mach-o/compact_unwind_encoding.h

                // Decode the variable-based permutation number into its digits. Each digit represents
                // an index into the list of register numbers that weren't yet used in the sequence at
                // the time the digit was added.
                const reg_count = frameless.stack_reg_count;
                const ip_ptr = ip_ptr: {
                    var digits: [6]u3 = undefined;
                    var accumulator: usize = frameless.stack_reg_permutation;
                    var base: usize = 2;
                    for (0..reg_count) |i| {
                        const div = accumulator / base;
                        digits[digits.len - 1 - i] = @intCast(accumulator - base * div);
                        accumulator = div;
                        base += 1;
                    }

                    var registers: [6]u3 = undefined;
                    var used_indices: [6]bool = @splat(false);
                    for (digits[digits.len - reg_count ..], 0..) |target_unused_index, i| {
                        var unused_count: u8 = 0;
                        const unused_index = for (used_indices, 0..) |used, index| {
                            if (!used) {
                                if (target_unused_index == unused_count) break index;
                                unused_count += 1;
                            }
                        } else unreachable;
                        registers[i] = @intCast(unused_index + 1);
                        used_indices[unused_index] = true;
                    }

                    var reg_addr = sp + stack_size - @sizeOf(usize) * @as(usize, reg_count + 1);
                    for (0..reg_count) |i| {
                        const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(registers[i]);
                        (try dwarfRegNative(&context.cpu_state, reg_number)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
                        reg_addr += @sizeOf(usize);
                    }

                    break :ip_ptr reg_addr;
                };

                const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
                const new_sp = ip_ptr + @sizeOf(usize);

                (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
                (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;

                break :ip new_ip;
            },
            .DWARF => {
                const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
                const rules = try context.computeRules(gpa, dwarf, unwind.vmaddr_slide, encoding.value.x86_64.dwarf);
                return context.next(gpa, &rules);
            },
        },
        .aarch64 => switch (encoding.mode.arm64) {
            .OLD => return error.UnsupportedDebugInfo,
            .FRAMELESS => ip: {
                const sp = (try dwarfRegNative(&context.cpu_state, sp_reg_num)).*;
                const new_sp = sp + encoding.value.arm64.frameless.stack_size * 16;
                const new_ip = (try dwarfRegNative(&context.cpu_state, 30)).*;
                (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
                break :ip new_ip;
            },
            .DWARF => {
                const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
                const rules = try context.computeRules(gpa, dwarf, unwind.vmaddr_slide, encoding.value.arm64.dwarf);
                return context.next(gpa, &rules);
            },
            .FRAME => ip: {
                const frame = encoding.value.arm64.frame;

                const fp = (try dwarfRegNative(&context.cpu_state, fp_reg_num)).*;
                const ip_ptr = fp + @sizeOf(usize);

                var reg_addr = fp - @sizeOf(usize);
                inline for (@typeInfo(@TypeOf(frame.x_reg_pairs)).@"struct".fields, 0..) |field, i| {
                    if (@field(frame.x_reg_pairs, field.name) != 0) {
                        (try dwarfRegNative(&context.cpu_state, 19 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
                        reg_addr += @sizeOf(usize);
                        (try dwarfRegNative(&context.cpu_state, 20 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
                        reg_addr += @sizeOf(usize);
                    }
                }

                // We intentionally skip restoring `frame.d_reg_pairs`; we know we don't support
                // vector registers in the AArch64 `cpu_context` anyway, so there's no reason to
                // fail a legitimate unwind just because we're asked to restore the registers here.
                // If some weird/broken unwind info tells us to read them later, we will fail then.
                reg_addr += 16 * @as(usize, @popCount(@as(u4, @bitCast(frame.d_reg_pairs))));

                const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
                const new_fp = @as(*const usize, @ptrFromInt(fp)).*;

                (try dwarfRegNative(&context.cpu_state, fp_reg_num)).* = new_fp;
                (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;

                break :ip new_ip;
            },
        },
        else => comptime unreachable, // unimplemented
    };

    const ret_addr = std.debug.stripInstructionPtrAuthCode(new_ip);

    // Like `Dwarf.SelfUnwinder.next`, adjust our next lookup pc in case the `call` was this
    // function's last instruction making `ret_addr` one byte past its end.
    context.pc = ret_addr -| 1;

    return ret_addr;
}

/// Acquires the mutex on success.
fn findModule(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!*Module {
    // This function is marked as deprecated; however, it is significantly more
    // performant than `dladdr` (since the latter also does a very slow symbol
    // lookup), so let's use it since it's still available.
    const text_base = std.c._dyld_get_image_header_containing_address(
        @ptrFromInt(address),
    ) orelse return error.MissingDebugInfo;
    try si.mutex.lock(io);
    errdefer si.mutex.unlock(io);
    const gop = try si.modules.getOrPutAdapted(gpa, @intFromPtr(text_base), Module.Adapter{});
    errdefer comptime unreachable;
    if (!gop.found_existing) gop.key_ptr.* = .{
        .text_base = @intFromPtr(text_base),
        .unwind = null,
        .file = null,
    };
    return gop.key_ptr;
}

const Module = struct {
    text_base: usize,
    unwind: ?(Error!Unwind),
    file: ?(Error!MachOFile),

    const Adapter = struct {
        pub fn hash(_: Adapter, text_base: usize) u32 {
            return @truncate(std.hash.int(text_base));
        }
        pub fn eql(_: Adapter, a_text_base: usize, b_module: Module, b_index: usize) bool {
            _ = b_index;
            return a_text_base == b_module.text_base;
        }
    };
    const Context = struct {
        pub fn hash(_: Context, module: Module) u32 {
            return @truncate(std.hash.int(module.text_base));
        }
        pub fn eql(_: Context, a_module: Module, b_module: Module, b_index: usize) bool {
            _ = b_index;
            return a_module.text_base == b_module.text_base;
        }
    };

    const Unwind = struct {
        /// The slide applied to the `__unwind_info` and `__eh_frame` sections.
        /// So, `unwind_info.ptr` is this many bytes higher than the section's vmaddr.
        vmaddr_slide: u64,
        /// Backed by the in-memory section mapped by the loader.
        unwind_info: ?[]const u8,
        /// Backed by the in-memory `__eh_frame` section mapped by the loader.
        dwarf: ?Dwarf.Unwind,
    };

    fn getUnwindInfo(module: *Module, gpa: Allocator) Error!*Unwind {
        if (module.unwind == null) module.unwind = loadUnwindInfo(module, gpa);
        return if (module.unwind.?) |*unwind| unwind else |err| err;
    }
    fn loadUnwindInfo(module: *const Module, gpa: Allocator) Error!Unwind {
        const header: *std.macho.mach_header_64 = @ptrFromInt(module.text_base);

        const raw_macho: [*]u8 = @ptrCast(header);
        var it = macho.LoadCommandIterator.init(header, raw_macho[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds]) catch unreachable;
        const sections, const text_vmaddr = while (it.next() catch unreachable) |load_cmd| {
            if (load_cmd.hdr.cmd != .SEGMENT_64) continue;
            const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
            if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
            break .{ load_cmd.getSections(), segment_cmd.vmaddr };
        } else unreachable;

        const vmaddr_slide = module.text_base - text_vmaddr;

        var opt_unwind_info: ?[]const u8 = null;
        var opt_eh_frame: ?[]const u8 = null;
        for (sections) |sect| {
            if (mem.eql(u8, sect.sectName(), "__unwind_info")) {
                const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
                opt_unwind_info = sect_ptr[0..@intCast(sect.size)];
            } else if (mem.eql(u8, sect.sectName(), "__eh_frame")) {
                const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
                opt_eh_frame = sect_ptr[0..@intCast(sect.size)];
            }
        }
        const eh_frame = opt_eh_frame orelse return .{
            .vmaddr_slide = vmaddr_slide,
            .unwind_info = opt_unwind_info,
            .dwarf = null,
        };
        var dwarf: Dwarf.Unwind = .initSection(.eh_frame, @intFromPtr(eh_frame.ptr) - vmaddr_slide, eh_frame);
        errdefer dwarf.deinit(gpa);
        // We don't need lookups, so this call is just for scanning CIEs.
        dwarf.prepare(gpa, @sizeOf(usize), native_endian, false, true) catch |err| switch (err) {
            error.ReadFailed => unreachable, // it's all fixed buffers
            error.InvalidDebugInfo,
            error.MissingDebugInfo,
            error.OutOfMemory,
            => |e| return e,
            error.EndOfStream,
            error.Overflow,
            error.StreamTooLong,
            error.InvalidOperand,
            error.InvalidOpcode,
            error.InvalidOperation,
            => return error.InvalidDebugInfo,
            error.UnsupportedAddrSize,
            error.UnsupportedDwarfVersion,
            error.UnimplementedUserOpcode,
            => return error.UnsupportedDebugInfo,
        };

        return .{
            .vmaddr_slide = vmaddr_slide,
            .unwind_info = opt_unwind_info,
            .dwarf = dwarf,
        };
    }

    fn getFile(module: *Module, gpa: Allocator, io: Io) Error!*MachOFile {
        if (module.file == null) {
            const path = std.mem.span(
                std.c.dyld_image_path_containing_address(@ptrFromInt(module.text_base)).?,
            );
            module.file = MachOFile.load(gpa, io, path, builtin.cpu.arch) catch |err| switch (err) {
                error.InvalidMachO, error.InvalidDwarf => error.InvalidDebugInfo,
                error.MissingDebugInfo, error.OutOfMemory, error.UnsupportedDebugInfo, error.ReadFailed => |e| e,
            };
        }
        return if (module.file.?) |*f| f else |err| err;
    }
};

const MachoSymbol = struct {
    strx: u32,
    addr: u64,
    /// Value may be `unknown_ofile`.
    ofile: u32,
    const unknown_ofile = std.math.maxInt(u32);
    fn addressLessThan(context: void, lhs: MachoSymbol, rhs: MachoSymbol) bool {
        _ = context;
        return lhs.addr < rhs.addr;
    }
    /// Assumes that `symbols` is sorted in order of ascending `addr`.
    fn find(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
        if (symbols.len == 0) return null; // no potential match
        if (address < symbols[0].addr) return null; // address is before the lowest-address symbol
        var left: usize = 0;
        var len: usize = symbols.len;
        while (len > 1) {
            const mid = left + len / 2;
            if (address < symbols[mid].addr) {
                len /= 2;
            } else {
                left = mid;
                len -= len / 2;
            }
        }
        return &symbols[left];
    }

    test find {
        const symbols: []const MachoSymbol = &.{
            .{ .addr = 100, .strx = undefined, .ofile = undefined },
            .{ .addr = 200, .strx = undefined, .ofile = undefined },
            .{ .addr = 300, .strx = undefined, .ofile = undefined },
        };

        try testing.expectEqual(null, find(symbols, 0));
        try testing.expectEqual(null, find(symbols, 99));
        try testing.expectEqual(&symbols[0], find(symbols, 100).?);
        try testing.expectEqual(&symbols[0], find(symbols, 150).?);
        try testing.expectEqual(&symbols[0], find(symbols, 199).?);

        try testing.expectEqual(&symbols[1], find(symbols, 200).?);
        try testing.expectEqual(&symbols[1], find(symbols, 250).?);
        try testing.expectEqual(&symbols[1], find(symbols, 299).?);

        try testing.expectEqual(&symbols[2], find(symbols, 300).?);
        try testing.expectEqual(&symbols[2], find(symbols, 301).?);
        try testing.expectEqual(&symbols[2], find(symbols, 5000).?);
    }
};
test {
    _ = MachoSymbol;
}

/// Uses `mmap` to map the file at `path` into memory.
fn mapDebugInfoFile(io: Io, path: []const u8) ![]align(std.heap.page_size_min) const u8 {
    const file = Io.Dir.cwd().openFile(io, path, .{}) catch |err| switch (err) {
        error.FileNotFound => return error.MissingDebugInfo,
        else => return error.ReadFailed,
    };
    defer file.close(io);

    const file_end_pos = file.length(io) catch |err| switch (err) {
        error.Unexpected => |e| return e,
        else => return error.ReadFailed,
    };
    const file_len = std.math.cast(usize, file_end_pos) orelse return error.InvalidDebugInfo;

    return posix.mmap(
        null,
        file_len,
        .{ .READ = true },
        .{ .TYPE = .SHARED },
        file.handle,
        0,
    ) catch |err| switch (err) {
        error.Unexpected => |e| return e,
        else => return error.ReadFailed,
    };
}

const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const Dwarf = std.debug.Dwarf;
const Error = std.debug.SelfInfoError;
const MachOFile = std.debug.MachOFile;
const assert = std.debug.assert;
const posix = std.posix;
const macho = std.macho;
const mem = std.mem;
const testing = std.testing;
const dwarfRegNative = std.debug.Dwarf.SelfUnwinder.regNative;

const builtin = @import("builtin");
const native_endian = builtin.target.cpu.arch.endian();

const SelfInfo = @This();
