From 243fb9184f1c67d69d54e11a80447f2d054a4ff1 Mon Sep 17 00:00:00 2001 From: bbodi Date: Thu, 8 Oct 2026 15:29:58 +0200 Subject: [PATCH 1/6] Define enum, typeid and #soa debug types locally With the split debug types modules, every other module only declares a record type. gdb resolves a declared struct by searching for its definition, but a declared enum by taking the first symbol of that name, which is the declaration itself in the module being looked at. Enums, flag enum bit_sets and typeid showed as , and every `any` printed as nil since its `id` could not be read. gdb also cannot find a declared type whose name starts with `#`, so #soa types were incomplete too. On DWARF, define these in every module that uses them, as clang does with enums. CodeView resolves forward references by name, so Windows keeps them in the types modules. --- src/llvm_backend_debug.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/llvm_backend_debug.cpp b/src/llvm_backend_debug.cpp index 50d6fd1d7..8c7897105 100644 --- a/src/llvm_backend_debug.cpp +++ b/src/llvm_backend_debug.cpp @@ -1294,6 +1294,22 @@ gb_internal LLVMMetadataRef lb_get_base_scope_metadata(lbModule *m, Scope *scope } } +// NOTE: gdb looks a declared enum up by name and stops at the first declaration it finds rather than the definition, +// and it cannot look up a declared type whose name starts with `#`, so on DWARF these are defined in every module +// which uses them, as clang does with enums +gb_internal bool lb_debug_type_is_defined_everywhere(Type *bt) { + if (build_context.metrics.os == TargetOs_windows) { + return false; + } + switch (bt->kind) { + case Type_Enum: return true; + case Type_BitSet: return lb_debug_bit_set_is_flag_enum(bt); + case Type_Struct: return bt->Struct.soa_kind != StructSoa_None; + case Type_Basic: return bt->Basic.kind == Basic_typeid; + } + return false; +} + gb_internal LLVMMetadataRef lb_debug_type(lbModule *m, Type *type) { GB_ASSERT(type != nullptr); @@ -1352,7 +1368,7 @@ gb_internal LLVMMetadataRef lb_debug_type(lbModule *m, Type *type) { Array const &types_modules = m->gen->debug_types_modules; String record_name = {}; lbModule *owner = nullptr; - if (is_record && types_modules.count != 0 && record_bt->kind != Type_Tuple) { + if (is_record && types_modules.count != 0 && record_bt->kind != Type_Tuple && !lb_debug_type_is_defined_everywhere(record_bt)) { record_name = type_to_canonical_string(temporary_allocator(), type); owner = types_modules[string_hash(record_name) % types_modules.count]; } From e22ae450af13eddde93fb353a3bad7903f2f7e7d Mon Sep 17 00:00:00 2001 From: bbodi Date: Thu, 8 Oct 2026 15:30:28 +0200 Subject: [PATCH 2/6] Show a union holding a pointer in the gdb printer A non-nil `Maybe(^T)` or a #shared_nil union of pointers printed as nothing, as gdb prints nothing for a pointer gdb.Value returned by a printer's to_string. Return the pointer formatted as a string instead, which is what the lldb summary already shows. --- base/runtime/odin_debugger.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/base/runtime/odin_debugger.py b/base/runtime/odin_debugger.py index ff16b7a87..158575458 100644 --- a/base/runtime/odin_debugger.py +++ b/base/runtime/odin_debugger.py @@ -481,7 +481,11 @@ if gdb is not None: return "<%s>" % e if name is None or (tag is None and int(self._val[name]) == 0): return "nil" - return self._val[name] + value = self._val[name] + if value.type.strip_typedefs().code == gdb.TYPE_CODE_PTR: + # gdb prints nothing for a pointer value returned by `to_string` + return value.format_string() + return value class _GdbAny(_GdbPrinter): # The value it holds, after the name of its type; an unknown type shows the `any` itself From e3c69ec54085139edfdb84d843821de03a6cc94f Mon Sep 17 00:00:00 2001 From: bbodi Date: Thu, 8 Oct 2026 15:31:11 +0200 Subject: [PATCH 3/6] Fix the debug name and characters of cstring16 The name `cstring16` was passed with the length of `cstring`, so the type was named `cstring`. Its characters, and those of `string16`, were a 16-bit `wchar_t`, which gdb reads as UTF-32 on Linux and so printed as garbage. On DWARF describe them as `char16_t`, a 16-bit UTF character, which gdb reads as UTF-16. CodeView keeps `wchar_t`. --- src/llvm_backend_debug.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/llvm_backend_debug.cpp b/src/llvm_backend_debug.cpp index 8c7897105..55239519b 100644 --- a/src/llvm_backend_debug.cpp +++ b/src/llvm_backend_debug.cpp @@ -884,6 +884,14 @@ gb_internal LLVMMetadataRef lb_debug_typeid_enum(lbModule *m) { ); } +// NOTE: gdb reads a `wchar_t` as UTF-32 on Linux, but a 16-bit UTF character as UTF-16; CodeView knows `wchar_t` as UTF-16 +gb_internal LLVMMetadataRef lb_debug_char16_type(lbModule *m) { + if (build_context.metrics.os == TargetOs_windows) { + return lb_debug_type_basic_type(m, str_lit("wchar_t"), 16, LLVMDWARFTypeEncoding_Unsigned); + } + return lb_debug_type_basic_type(m, str_lit("char16_t"), 16, LLVMDWARFTypeEncoding_Utf); +} + gb_internal LLVMMetadataRef lb_debug_type_internal(lbModule *m, Type *type) { i64 size = type_size_of(type); // Check size gb_unused(size); @@ -1034,8 +1042,8 @@ gb_internal LLVMMetadataRef lb_debug_type_internal(lbModule *m, Type *type) { case Basic_string16: { // NOTE(bill): size_of(^u16) <= size_of(int) - // The data is `^wchar_t`, as `cstring16` is, so that debuggers show it as text - LLVMMetadataRef char_type = lb_debug_type_basic_type(m, str_lit("wchar_t"), 16, LLVMDWARFTypeEncoding_Unsigned); + // The data is a pointer to a UTF-16 character, as `cstring16` is, so that debuggers show it as text + LLVMMetadataRef char_type = lb_debug_char16_type(m); LLVMMetadataRef file = lb_get_file_metadata(m, m->info->runtime_package->files[0]); LLVMMetadataRef elements[2] = {}; @@ -1047,8 +1055,8 @@ gb_internal LLVMMetadataRef lb_debug_type_internal(lbModule *m, Type *type) { } case Basic_cstring16: { - LLVMMetadataRef char_type = lb_debug_type_basic_type(m, str_lit("wchar_t"), 16, LLVMDWARFTypeEncoding_Unsigned); - return LLVMDIBuilderCreatePointerType(m->debug_builder, char_type, ptr_bits, ptr_bits, 0, "cstring16", 7); + LLVMMetadataRef char_type = lb_debug_char16_type(m); + return LLVMDIBuilderCreatePointerType(m->debug_builder, char_type, ptr_bits, ptr_bits, 0, "cstring16", 9); } case Basic_any: From a3c940fc019b6d980e07a75790bb014d363c58cd Mon Sep 17 00:00:00 2001 From: bbodi Date: Thu, 8 Oct 2026 15:31:57 +0200 Subject: [PATCH 4/6] Show an `any` holding a pointer or an array An `any` holding a pointer, multi-pointer or fixed array printed raw, as `{data = ..., id = ^int}`, since no debug type is named `^int` or `[3]f32`. Build such types from their element, found by the rest of the name, in both the gdb and lldb printers. --- base/runtime/odin_debugger.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/base/runtime/odin_debugger.py b/base/runtime/odin_debugger.py index 158575458..83fbfed99 100644 --- a/base/runtime/odin_debugger.py +++ b/base/runtime/odin_debugger.py @@ -111,6 +111,19 @@ def odin_kind(type_name, field_names, data_is_pointer, data_is_array, data_targe return None +def composite_type(name): + # the element of a pointer, multi-pointer or fixed array type named by a `typeid` and the array's count, + # which is None for a pointer; None for other types + if name.startswith("^"): + return name[1:], None + if name.startswith("[^]"): + return name[3:], None + m = re.fullmatch(r"\[([0-9]+)\](.+)", name) + if m: + return m.group(2), int(m.group(1)) + return None + + def soa_fields(field_names): return [n for n in field_names if n not in ("__$len", "__$cap", "allocator", "_")] @@ -278,6 +291,16 @@ if gdb is not None: return None def _gdb_odin_type(name): + composite = composite_type(name) + if composite is not None: + elem_name, count = composite + elem = _gdb_odin_type(elem_name) + if elem is None: + return None + if count is None: + return elem.pointer() + return elem.array(count - 1) + basic = ODIN_BASIC_TYPES.get(name) if basic is None: return _gdb_type(name, "struct " + name, "union " + name, "enum " + name) @@ -575,6 +598,16 @@ if lldb is not None: def _lldb_odin_type(target, name, id): # the type a `typeid` names, None when it cannot be found + composite = composite_type(name) + if composite is not None and typeid_hash(name) == id: + elem_name, count = composite + elem = _lldb_odin_type(target, elem_name, typeid_hash(elem_name)) + if elem is None: + return None + if count is None: + return elem.GetPointerType() + return elem.GetArrayType(count) + basic = ODIN_BASIC_TYPES.get(name) if basic is None: types = target.FindTypes(name) @@ -664,7 +697,7 @@ if lldb is not None: t = _lldb_odin_type(valobj.GetTarget(), name, id.GetValueAsUnsigned()) if t is None: return name, None - if name not in ODIN_BASIC_TYPES: + if name not in ODIN_BASIC_TYPES and composite_type(name) is None: name = t.GetName() value = valobj.CreateValueFromAddress("value", data, t) value.SetPreferSyntheticValue(True) From ed6b1dd5cd305d7f14d71e4dd5f4053ee38910f5 Mon Sep 17 00:00:00 2001 From: bbodi Date: Thu, 8 Oct 2026 15:32:26 +0200 Subject: [PATCH 5/6] Describe rawptr as a pointer to no type `rawptr` pointed to a 1-byte `void` base type, so gdb printed every rawptr, such as an allocator's `data`, as a C string. Describe it as a pointer with no pointee type, which is how clang emits `void *`. This also drops the address space it was given by mistake. --- src/llvm_backend_debug.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/llvm_backend_debug.cpp b/src/llvm_backend_debug.cpp index 55239519b..db06de68d 100644 --- a/src/llvm_backend_debug.cpp +++ b/src/llvm_backend_debug.cpp @@ -1021,8 +1021,8 @@ gb_internal LLVMMetadataRef lb_debug_type_internal(lbModule *m, Type *type) { case Basic_rawptr: { - LLVMMetadataRef void_type = lb_debug_type_basic_type(m, str_lit("void"), 8, LLVMDWARFTypeEncoding_Unsigned); - return LLVMDIBuilderCreatePointerType(m->debug_builder, void_type, ptr_bits, ptr_bits, LLVMDWARFTypeEncoding_Address, "rawptr", 6); + // NOTE: a pointer to no type is `void *`, as clang emits it, rather than a pointer to a byte shown as a C string + return LLVMDIBuilderCreatePointerType(m->debug_builder, nullptr, ptr_bits, ptr_bits, 0, "rawptr", 6); } case Basic_string: { From 3f5741c6b2b514dbd016ad45ef78df81fd3e1edc Mon Sep 17 00:00:00 2001 From: BB Date: Thu, 8 Oct 2026 16:07:18 +0200 Subject: [PATCH 6/6] Take an `any`'s basic types from Odin's type system Apple's lldb includes Swift, and `SBTarget.GetBasicType` may answer from Swift's type system, such as `Swift.Int64` for `long long`. A value made from such a type is empty, so in about half of all lldb sessions an `any` holding a basic type, or a pointer or an array of one, showed only the name of its type. Ask the `any`'s own type for basic types instead, as it comes from the type system of Odin's debug info. Co-Authored-By: Claude Opus 5.5 (1M context) --- base/runtime/odin_debugger.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/base/runtime/odin_debugger.py b/base/runtime/odin_debugger.py index 83fbfed99..38974387a 100644 --- a/base/runtime/odin_debugger.py +++ b/base/runtime/odin_debugger.py @@ -596,12 +596,13 @@ if lldb is not None: cell_data_size = _lldb_fields(cell)["v"].GetByteSize() return cell_layout(elem.GetByteSize(), cell.GetByteSize(), cell_data_size) - def _lldb_odin_type(target, name, id): - # the type a `typeid` names, None when it cannot be found + def _lldb_odin_type(target, basis, name, id): + # the type a `typeid` names, None when it cannot be found; basic types come from the type system of `basis`, + # an Odin type, as the target may answer from another, such as Swift's, whose types make no value composite = composite_type(name) if composite is not None and typeid_hash(name) == id: elem_name, count = composite - elem = _lldb_odin_type(target, elem_name, typeid_hash(elem_name)) + elem = _lldb_odin_type(target, basis, elem_name, typeid_hash(elem_name)) if elem is None: return None if count is None: @@ -622,11 +623,11 @@ if lldb is not None: size = target.GetAddressByteSize() if kind == "rawptr": - return target.GetBasicType(lldb.eBasicTypeVoid).GetPointerType() + return basis.GetBasicType(lldb.eBasicTypeVoid).GetPointerType() if kind == "cstring": - return target.GetBasicType(lldb.eBasicTypeChar).GetPointerType() + return basis.GetBasicType(lldb.eBasicTypeChar).GetPointerType() if kind == "cstring16": - return target.GetBasicType(lldb.eBasicTypeChar16).GetPointerType() + return basis.GetBasicType(lldb.eBasicTypeChar16).GetPointerType() basics = { ("int", 1): lldb.eBasicTypeSignedChar, @@ -648,7 +649,7 @@ if lldb is not None: basic_type = basics.get((kind, size)) if basic_type is None: return None - return target.GetBasicType(basic_type) + return basis.GetBasicType(basic_type) def lldb_is_string(sbtype, internal_dict): return _lldb_kind(sbtype) in ("string", "string16") @@ -694,7 +695,7 @@ if lldb is not None: name = id.GetValue() or "" if data == 0 or id.GetValueAsUnsigned() == 0: return "nil", None - t = _lldb_odin_type(valobj.GetTarget(), name, id.GetValueAsUnsigned()) + t = _lldb_odin_type(valobj.GetTarget(), v.GetType(), name, id.GetValueAsUnsigned()) if t is None: return name, None if name not in ODIN_BASIC_TYPES and composite_type(name) is None: