llvm: some more improvements to debug info

Most importantly, adds support for `DW_TAG_typedef` to `llvm.Builder`,
and uses it to define error sets and optional pointers/errors.

Also deletes some random dead code I found.
This commit is contained in:
Matthew Lugg 2026-02-13 11:48:56 +00:00
parent a6bee423d2
commit e88727390f
No known key found for this signature in database
GPG key ID: 3F5B7DCCBF4AF02E
2 changed files with 142 additions and 173 deletions

View file

@ -8021,6 +8021,7 @@ pub const Metadata = packed struct(u32) {
composite_vector_type,
derived_pointer_type,
derived_member_type,
derived_typedef_type,
subroutine_type,
enumerator_unsigned,
enumerator_signed_positive,
@ -8064,6 +8065,7 @@ pub const Metadata = packed struct(u32) {
.composite_vector_type,
.derived_pointer_type,
.derived_member_type,
.derived_typedef_type,
.subroutine_type,
.enumerator_unsigned,
.enumerator_signed_positive,
@ -10463,15 +10465,18 @@ pub fn print(self: *Builder, w: *Writer) (Writer.Error || Allocator.Error)!void
},
.derived_pointer_type,
.derived_member_type,
.derived_typedef_type,
=> |kind| {
const extra = self.metadataExtraData(Metadata.DerivedType, metadata_item.data);
try metadata_formatter.specialized(.@"!", .DIDerivedType, .{
.tag = @as(enum {
DW_TAG_pointer_type,
DW_TAG_member,
DW_TAG_typedef,
}, switch (kind) {
.derived_pointer_type => .DW_TAG_pointer_type,
.derived_member_type => .DW_TAG_member,
.derived_typedef_type => .DW_TAG_typedef,
else => unreachable,
}),
.name = extra.name,
@ -12360,6 +12365,30 @@ pub fn debugMemberType(
);
}
pub fn debugTypedefType(
self: *Builder,
name: ?Metadata.String,
file: ?Metadata,
scope: ?Metadata,
line: u32,
underlying_type: ?Metadata,
size_in_bits: u64,
align_in_bits: u64,
offset_in_bits: u64,
) Allocator.Error!Metadata {
try self.ensureUnusedMetadataCapacity(1, Metadata.DerivedType, 0);
return self.debugTypedefTypeAssumeCapacity(
name,
file,
scope,
line,
underlying_type,
size_in_bits,
align_in_bits,
offset_in_bits,
);
}
pub fn debugSubroutineType(self: *Builder, types_tuple: ?Metadata) Allocator.Error!Metadata {
try self.ensureUnusedMetadataCapacity(1, Metadata.SubroutineType, 0);
return self.debugSubroutineTypeAssumeCapacity(types_tuple);
@ -12875,6 +12904,33 @@ fn debugMemberTypeAssumeCapacity(
});
}
fn debugTypedefTypeAssumeCapacity(
self: *Builder,
name: ?Metadata.String,
file: ?Metadata,
scope: ?Metadata,
line: u32,
underlying_type: ?Metadata,
size_in_bits: u64,
align_in_bits: u64,
offset_in_bits: u64,
) Metadata {
assert(!self.strip);
return self.metadataSimpleAssumeCapacity(.derived_typedef_type, Metadata.DerivedType{
.name = .wrap(name),
.file = .wrap(file),
.scope = .wrap(scope),
.line = line,
.underlying_type = .wrap(underlying_type),
.size_in_bits_lo = @truncate(size_in_bits),
.size_in_bits_hi = @truncate(size_in_bits >> 32),
.align_in_bits_lo = @truncate(align_in_bits),
.align_in_bits_hi = @truncate(align_in_bits >> 32),
.offset_in_bits_lo = @truncate(offset_in_bits),
.offset_in_bits_hi = @truncate(offset_in_bits >> 32),
});
}
fn debugSubroutineTypeAssumeCapacity(self: *Builder, types_tuple: ?Metadata) Metadata {
assert(!self.strip);
return self.metadataSimpleAssumeCapacity(.subroutine_type, Metadata.SubroutineType{
@ -14223,12 +14279,14 @@ pub fn toBitcode(self: *Builder, allocator: Allocator, producer: Producer) bitco
},
.derived_pointer_type,
.derived_member_type,
.derived_typedef_type,
=> |kind| {
const extra = self.metadataExtraData(Metadata.DerivedType, data);
try metadata_block.writeAbbrevAdapted(MetadataBlock.DerivedType{
.tag = switch (kind) {
.derived_pointer_type => DW.TAG.pointer_type,
.derived_member_type => DW.TAG.member,
.derived_typedef_type => DW.TAG.typedef,
else => unreachable,
},
.name = extra.name,

View file

@ -1914,6 +1914,15 @@ pub const Object = struct {
const name = try o.builder.metadataStringFmt("{f}", .{ty.fmt(pt)});
// lldb cannot handle non-byte-sized types, so in the logic below, bit sizes are padded up.
// For instance, `bool` is considered to be 8 bits, and `u60` is considered to be 64 bits.
// I tried using variants (DW_TAG_variant_part + DW_TAG_variant) to encode error unions,
// tagged unions, etc; this would have told debuggers which field was active, which could
// improve UX significantly. GDB handles this perfectly fine, but unfortunately, LLDB has no
// handling for variants at all, and will never print fields in them, so I opted not to use
// them for now.
switch (ty.zigTypeTag(zcu)) {
.void,
.noreturn,
@ -1925,23 +1934,19 @@ pub const Object = struct {
.enum_literal,
=> return o.builder.debugSignedType(name, 0),
.float => return o.builder.debugFloatType(name, ty.floatBits(target)),
.bool => return o.builder.debugBoolType(name, 8),
.int => {
const info = ty.intInfo(zcu);
const bits = ty.abiSize(zcu) * 8; // lldb cannot handle non-byte sized types
const bits = ty.abiSize(zcu) * 8;
return switch (info.signedness) {
.signed => try o.builder.debugSignedType(name, bits),
.unsigned => try o.builder.debugUnsignedType(name, bits),
};
},
.float => {
return o.builder.debugFloatType(name, ty.floatBits(target));
},
.bool => {
return o.builder.debugBoolType(
name,
8, // lldb cannot handle non-byte sized types
);
},
.pointer => {
const ptr_size = Type.ptrAbiSize(zcu.getTarget());
const ptr_align = Type.ptrAbiAlignment(zcu.getTarget());
@ -1949,20 +1954,20 @@ pub const Object = struct {
if (ty.isSlice(zcu)) {
const debug_ptr_type = try o.builder.debugMemberType(
try o.builder.metadataString("ptr"),
null, // File
null, // file
ty_fwd_ref,
0, // Line
0, // line
try o.getDebugType(pt, ty.slicePtrFieldType(zcu)),
ptr_size * 8,
ptr_align.toByteUnits().? * 8,
0, // Offset
0, // offset
);
const debug_len_type = try o.builder.debugMemberType(
try o.builder.metadataString("len"),
null, // File
null, // file
ty_fwd_ref,
0, // Line
0, // line
try o.getDebugType(pt, .usize),
ptr_size * 8,
ptr_align.toByteUnits().? * 8,
@ -1971,10 +1976,10 @@ pub const Object = struct {
return o.builder.debugStructType(
name,
null, // File
o.debug_compile_unit.unwrap().?, // Scope
0, // Line
null, // Underlying type
null, // file
o.debug_compile_unit.unwrap().?, // scope
0, // line
null, // underlying type
ptr_size * 2 * 8,
ptr_align.toByteUnits().? * 8,
try o.builder.metadataTuple(&.{
@ -1986,36 +1991,34 @@ pub const Object = struct {
return o.builder.debugPointerType(
name,
null, // File
null, // Scope
0, // Line
null, // file
o.debug_compile_unit.unwrap().?, // scope
0, // line
try o.getDebugType(pt, ty.childType(zcu)),
ptr_size * 8,
ptr_align.toByteUnits().? * 8,
0, // Offset
);
},
.array => {
return o.builder.debugArrayType(
null, // Name
null, // File
null, // Scope
0, // Line
try o.getDebugType(pt, ty.childType(zcu)),
ty.abiSize(zcu) * 8,
(ty.abiAlignment(zcu).toByteUnits() orelse 0) * 8,
try o.builder.metadataTuple(&.{
try o.builder.debugSubrange(
try o.builder.metadataConstant(try o.builder.intConst(.i64, 0)),
try o.builder.metadataConstant(try o.builder.intConst(.i64, ty.arrayLen(zcu))),
),
}),
0, // offset
);
},
.array => return o.builder.debugArrayType(
name,
null, // file
o.debug_compile_unit.unwrap().?, // scope
0, // line
try o.getDebugType(pt, ty.childType(zcu)),
ty.abiSize(zcu) * 8,
ty.abiAlignment(zcu).toByteUnits().? * 8,
try o.builder.metadataTuple(&.{
try o.builder.debugSubrange(
try o.builder.metadataConstant(try o.builder.intConst(.i64, 0)),
try o.builder.metadataConstant(try o.builder.intConst(.i64, ty.arrayLen(zcu))),
),
}),
),
.vector => {
const elem_ty = ty.childType(zcu);
// Vector elements cannot be padded since that would make
// @bitSizOf(elem) * len > @bitSizOf(vec).
// @bitSizeOf(elem) * len > @bitSizOf(vec).
// Neither gdb nor lldb seem to be able to display non-byte sized
// vectors properly.
const debug_elem_type = switch (elem_ty.zigTypeTag(zcu)) {
@ -2027,18 +2030,19 @@ pub const Object = struct {
};
},
.bool => try o.builder.debugBoolType(try o.builder.metadataString("bool"), 1),
// We don't pad pointers or floats, so we can lower those normally.
.pointer, .optional, .float => try o.getDebugType(pt, elem_ty),
else => unreachable,
};
return o.builder.debugVectorType(
null, // Name
null, // File
null, // Scope
0, // Line
name,
null, // file
o.debug_compile_unit.unwrap().?, // scope
0, // line
debug_elem_type,
ty.abiSize(zcu) * 8,
(ty.abiAlignment(zcu).toByteUnits() orelse 0) * 8,
ty.abiAlignment(zcu).toByteUnits().? * 8,
try o.builder.metadataTuple(&.{
try o.builder.debugSubrange(
try o.builder.metadataConstant(try o.builder.intConst(.i64, 0)),
@ -2050,26 +2054,15 @@ pub const Object = struct {
.optional => {
const payload_ty = ty.optionalChild(zcu);
if (ty.optionalReprIsPayload(zcu)) {
// MLUGG TODO: these should use DW_TAG_typedef instead, but std.zig.llvm.Builder currently lacks support for those.
const payload_member = try o.builder.debugMemberType(
try o.builder.metadataString("payload"),
null, // file
ty_fwd_ref,
0, // line
try o.getDebugType(pt, .anyerror),
ty.abiSize(zcu) * 8,
ty.abiAlignment(zcu).toByteUnits().? * 8,
0, // offset
);
return o.builder.debugStructType(
return o.builder.debugTypedefType(
name,
null, // file
o.debug_compile_unit.unwrap().?, // scope
0, // line
null, // underlying type
try o.getDebugType(pt, payload_ty),
ty.abiSize(zcu) * 8,
ty.abiAlignment(zcu).toByteUnits().? * 8,
try o.builder.metadataTuple(&.{payload_member}),
0, // offset
);
}
@ -2083,7 +2076,7 @@ pub const Object = struct {
const debug_payload_type = try o.builder.debugMemberType(
try o.builder.metadataString("payload"),
null, // file
ty_fwd_ref,
ty_fwd_ref, // scope
0, // line
try o.getDebugType(pt, payload_ty),
payload_size * 8,
@ -2104,12 +2097,12 @@ pub const Object = struct {
return o.builder.debugStructType(
name,
null, // File
o.debug_compile_unit.unwrap().?, // Scope
0, // Line
null, // Underlying type
null, // file
o.debug_compile_unit.unwrap().?, // scope
0, // line
null, // underlying type
ty.abiSize(zcu) * 8,
(ty.abiAlignment(zcu).toByteUnits() orelse 0) * 8,
ty.abiAlignment(zcu).toByteUnits().? * 8,
try o.builder.metadataTuple(&.{
debug_payload_type,
debug_some_type,
@ -2125,30 +2118,29 @@ pub const Object = struct {
const payload_size = payload_ty.abiSize(zcu);
const payload_align = payload_ty.abiAlignment(zcu);
const error_index: u1, const payload_index: u1, const error_offset: u64, const payload_offset: u64 = fields: {
const error_offset: u64, const payload_offset: u64 = offsets: {
if (error_align.compare(.gt, payload_align)) {
break :fields .{ 0, 1, 0, payload_align.forward(error_size) };
break :offsets .{ 0, payload_align.forward(error_size) };
} else {
break :fields .{ 1, 0, error_align.forward(payload_size), 0 };
break :offsets .{ error_align.forward(payload_size), 0 };
}
};
var fields: [2]Builder.Metadata = undefined;
fields[error_index] = try o.builder.debugMemberType(
const error_field = try o.builder.debugMemberType(
try o.builder.metadataString("error"),
null, // File
null, // file
ty_fwd_ref,
0, // Line
0, // line
try o.getDebugType(pt, error_ty),
error_size * 8,
error_align.toByteUnits().? * 8,
error_offset * 8,
);
fields[payload_index] = try o.builder.debugMemberType(
const payload_field = try o.builder.debugMemberType(
try o.builder.metadataString("payload"),
null, // File
ty_fwd_ref,
0, // Line
null, // file
ty_fwd_ref, // scope
0, // line
try o.getDebugType(pt, payload_ty),
payload_size * 8,
payload_align.toByteUnits().? * 8,
@ -2163,33 +2155,22 @@ pub const Object = struct {
null, // Underlying type
ty.abiSize(zcu) * 8,
ty.abiAlignment(zcu).toByteUnits().? * 8,
try o.builder.metadataTuple(&fields),
try o.builder.metadataTuple(&.{ error_field, payload_field }),
);
},
.error_set => {
assert(ty.toIntern() != .anyerror_type); // handled specially in `updateConst`; will be populated by `emit` instead
// Error sets are just named wrappers around `anyerror`.
// MLUGG TODO: these should use DW_TAG_typedef instead, but std.zig.llvm.Builder currently lacks support for those.
const anyerror_member = try o.builder.debugMemberType(
try o.builder.metadataString("error"),
return o.builder.debugTypedefType(
name,
null, // file
ty_fwd_ref,
o.debug_compile_unit.unwrap().?, // scope
0, // line
try o.getDebugType(pt, .anyerror),
ty.abiSize(zcu) * 8,
ty.abiAlignment(zcu).toByteUnits().? * 8,
0, // offset
);
return o.builder.debugStructType(
name,
null, // file
o.debug_compile_unit.unwrap().?, // scope
0, // line
null, // underlying type
ty.abiSize(zcu) * 8,
ty.abiAlignment(zcu).toByteUnits().? * 8,
try o.builder.metadataTuple(&.{anyerror_member}),
);
},
.@"fn" => {
if (!ty.fnHasRuntimeBits(zcu)) {
@ -2570,14 +2551,22 @@ pub const Object = struct {
const error_set_bits = zcu.errorSetBits();
const error_names = ip.global_error_set.getNamesFromMainThread();
const enumerators = try gpa.alloc(Builder.Metadata, error_names.len);
const enumerators = try gpa.alloc(Builder.Metadata, error_names.len + 1);
defer gpa.free(enumerators);
for (enumerators, error_names, 1..) |*out, error_name, error_value| {
// The value 0 means "no error" in optionals and error unions.
enumerators[0] = try o.builder.debugEnumerator(
try o.builder.metadataString("null"),
true, // unsigned,
error_set_bits,
.{ .limbs = &.{0}, .positive = true }, // zero
);
for (enumerators[1..], error_names, 1..) |*out, error_name, error_value| {
var space: Value.BigIntSpace = undefined;
var bigint: std.math.big.int.Mutable = .init(&space.limbs, error_value);
out.* = try o.builder.debugEnumerator(
try o.builder.metadataString(error_name.toSlice(ip)),
try o.builder.metadataStringFmt("error.{f}", .{error_name.fmtId(ip)}),
true, // unsigned
error_set_bits,
bigint.toConst(),
@ -3452,84 +3441,6 @@ pub const Object = struct {
);
}
fn lowerValueToInt(o: *Object, pt: Zcu.PerThread, llvm_int_ty: Builder.Type, arg_val: InternPool.Index) Error!Builder.Constant {
const zcu = pt.zcu;
const ip = &zcu.intern_pool;
const target = zcu.getTarget();
const val = Value.fromInterned(arg_val);
const val_key = ip.indexToKey(val.toIntern());
if (val.isUndef(zcu)) return o.builder.undefConst(llvm_int_ty);
const ty = Type.fromInterned(val_key.typeOf());
switch (val_key) {
.@"extern" => |@"extern"| {
const function_index = try o.resolveLlvmFunction(pt, @"extern".owner_nav);
const ptr = function_index.ptrConst(&o.builder).global.toConst();
return o.builder.convConst(ptr, llvm_int_ty);
},
.func => |func| {
const function_index = try o.resolveLlvmFunction(pt, func.owner_nav);
const ptr = function_index.ptrConst(&o.builder).global.toConst();
return o.builder.convConst(ptr, llvm_int_ty);
},
.ptr => return o.builder.convConst(try o.lowerPtr(pt, arg_val, 0), llvm_int_ty),
.aggregate => switch (ip.indexToKey(ty.toIntern())) {
.struct_type, .vector_type => {},
else => unreachable,
},
.un => |un| {
const layout = ty.unionGetLayout(zcu);
if (layout.payload_size == 0) return o.lowerValue(pt, un.tag);
const union_obj = zcu.typeToUnion(ty).?;
const container_layout = union_obj.layout;
assert(container_layout == .@"packed");
var need_unnamed = false;
if (un.tag == .none) {
assert(layout.tag_size == 0);
const union_val = try o.lowerValueToInt(pt, llvm_int_ty, un.val);
need_unnamed = true;
return union_val;
}
const field_index = zcu.unionTagFieldIndex(union_obj, Value.fromInterned(un.tag)).?;
const field_ty = Type.fromInterned(union_obj.field_types.get(ip)[field_index]);
if (!field_ty.hasRuntimeBits(zcu)) return o.builder.intConst(llvm_int_ty, 0);
return o.lowerValueToInt(pt, llvm_int_ty, un.val);
},
.simple_value => |simple_value| switch (simple_value) {
.false, .true => {},
else => unreachable,
},
.int,
.float,
.enum_tag,
=> {},
.opt => {}, // pointer like optional expected
else => unreachable,
}
var stack = std.heap.stackFallback(32, o.gpa);
const allocator = stack.get();
const bits: usize = @intCast(ty.bitSize(zcu));
const buffer = try allocator.alloc(u8, (bits + 7) / 8);
defer allocator.free(buffer);
const limbs = try allocator.alloc(std.math.big.Limb, std.math.big.int.calcTwosCompLimbCount(bits));
defer allocator.free(limbs);
val.writeToPackedMemory(pt, buffer, 0) catch unreachable;
var big: std.math.big.int.Mutable = .init(limbs, 0);
big.readTwosComplement(buffer, bits, target.cpu.arch.endian(), .unsigned);
return o.builder.bigIntConst(llvm_int_ty, big.toConst());
}
fn lowerValue(o: *Object, pt: Zcu.PerThread, arg_val: InternPool.Index) Error!Builder.Constant {
const zcu = pt.zcu;
const ip = &zcu.intern_pool;