Files
Odin/base/runtime/odin_debugger.py
T
2026-10-08 13:32:11 +01:00

937 lines
32 KiB
Python

# Pretty printers for Odin types in gdb and lldb
#
# gdb: source <odin>/base/runtime/odin_debugger.py
# lldb: command script import <odin>/base/runtime/odin_debugger.py
#
# gdb also loads it by itself from the `.debug_gdb_scripts` section of an ELF binary built with `-debug`,
# once that binary's directory is trusted, with `add-auto-load-safe-path <directory>`, and lldb from the dSYM
# of a macOS binary built with `-debug`, once `settings set target.load-script-from-symbol-file true`
#
# Shown: `string`, `string16`, slices, dynamic arrays (and fixed capacity ones), `#soa` slices and dynamic arrays,
# maps as their entries (a map of zero sized values as its keys), unions as the variant they hold, or `nil`,
# and `any` as the value it holds.
# Types are recognised by their fields rather than their names, so named types, such as
# `Table :: map[string]int`, are shown the same way.
#
# gdb: front ends which mishandle the "map" display hint, such as Microsoft's C/C++ extension for VS Code,
# need `set odin-map-hint off`, which names each entry by its key instead.
#
# A map's debug type does not describe its memory: `data` points to a struct whose fields give the
# `key`, `value` and `hash` types and how keys and values are packed into cells, `key_cell` and `value_cell`
# (see `init_map_internal_debug_types` in the compiler and `Raw_Map` in `base:runtime`).
# A union's members are its variants, `v<tag>`, and the `tag` itself, except for a `Maybe` of a pointer,
# which is `v0` alone and nil when the pointer is (see `lb_debug_union`).
# An `any`'s `id` is an enum named by each type's canonical name, which finds the type of what `data` points to;
# a `typeid` is the hash of that name, which picks the type when a debugger shortens the name (lldb does, from a PDB).
# Every target Odin supports is little-endian.
import re
MAX_STRING_BYTES = 64*1024
MAP_METADATA_FIELDS = ["key", "value", "hash", "key_cell", "value_cell"]
# the kind and size of each basic type, by its name in `typeid`; None is the size of a pointer
ODIN_BASIC_TYPES = {
"i8": ("int", 1), "i16": ("int", 2), "i32": ("int", 4), "i64": ("int", 8), "i128": ("int", 16),
"u8": ("uint", 1), "u16": ("uint", 2), "u32": ("uint", 4), "u64": ("uint", 8), "u128": ("uint", 16),
"i16le": ("int", 2), "i32le": ("int", 4), "i64le": ("int", 8), "i128le": ("int", 16),
"u16le": ("uint", 2), "u32le": ("uint", 4), "u64le": ("uint", 8), "u128le": ("uint", 16),
"f16": ("float", 2), "f32": ("float", 4), "f64": ("float", 8),
"f16le": ("float", 2), "f32le": ("float", 4), "f64le": ("float", 8),
"f16be": ("float", 2), "f32be": ("float", 4), "f64be": ("float", 8),
"bool": ("bool", 1), "b8": ("bool", 1), "b16": ("uint", 2), "b32": ("uint", 4), "b64": ("uint", 8),
"rune": ("rune", 4),
"int": ("int", None), "uint": ("uint", None), "uintptr": ("uint", None),
"rawptr": ("rawptr", None),
"cstring": ("cstring", None), "cstring16": ("cstring16", None),
}
def typeid_hash(name):
# SipHash-2-4 of a type's canonical name, which is its `typeid` (see `type_hash_canonical_type`)
mask = (1 << 64) - 1
k0 = 0xa6592ea25e04ac3c
k1 = 0xba3cba04ed28a9ae
v = [0x736f6d6570736575 ^ k0, 0x646f72616e646f6d ^ k1, 0x6c7967656e657261 ^ k0, 0x7465646279746573 ^ k1]
def rotl(x, k):
return ((x << k) | (x >> (64 - k))) & mask
def rounds(n):
for _ in range(n):
v[0] = (v[0] + v[1]) & mask; v[1] = rotl(v[1], 13); v[1] ^= v[0]; v[0] = rotl(v[0], 32)
v[2] = (v[2] + v[3]) & mask; v[3] = rotl(v[3], 16); v[3] ^= v[2]
v[0] = (v[0] + v[3]) & mask; v[3] = rotl(v[3], 21); v[3] ^= v[0]
v[2] = (v[2] + v[1]) & mask; v[1] = rotl(v[1], 17); v[1] ^= v[2]; v[2] = rotl(v[2], 32)
data = name.encode("utf-8")
full = len(data) & ~7
last = bytearray(data[full:]) + bytearray(8 - (len(data) - full))
last[7] = len(data) & 0xff
for block in [data[i:i+8] for i in range(0, full, 8)] + [bytes(last)]:
m = int.from_bytes(block, "little")
v[3] ^= m
rounds(2)
v[0] ^= m
v[2] ^= 0xff
rounds(4)
h = v[0] ^ v[1] ^ v[2] ^ v[3]
return h if h != 0 else 1
def is_map_metadata(field_names):
# the members of a zero sized value can be missing
if not all(n in MAP_METADATA_FIELDS for n in field_names):
return False
return all(n in field_names for n in ("key", "hash", "key_cell"))
def odin_kind(type_name, field_names, data_is_pointer, data_is_array, data_target_fields):
if field_names == ["data", "len"] and type_name in ("string", "string16"):
return type_name
if field_names == ["data", "id"] and type_name == "any":
return "any"
if field_names == ["data", "len", "allocator"] and is_map_metadata(data_target_fields):
return "map"
if field_names == ["data", "len", "cap", "allocator"] and data_is_pointer:
return "dynamic"
if field_names == ["data", "len"] and data_is_pointer:
return "slice"
if field_names == ["data", "len"] and data_is_array:
return "fixed"
if field_names[-3:] == ["__$len", "__$cap", "allocator"]:
return "soa_dynamic"
if field_names[-1:] == ["__$len"]:
return "soa_slice"
return None
def soa_fields(field_names):
return [n for n in field_names if n not in ("__$len", "__$cap", "allocator", "_")]
def is_odin_union(field_names):
variants = [n for n in field_names if n != "tag"]
return len(variants) != 0 and all(re.fullmatch(r"v[0-9]+", n) for n in variants)
def union_variant(field_names, tag):
# the member holding the union's value, None when it is nil; `tag` is None for a `Maybe` of a pointer
if tag is None:
return "v0"
name = "v%d" % tag
if name in field_names:
return name
if tag == 0:
return None
raise ValueError("invalid tag %d" % tag)
def cell_layout(elem_size, cell_size, cell_data_size):
# A cell is the element type itself when packing needs no padding,
# else `struct{v: [n]T, _: [padding]u8}`, where `cell_data_size` is the size of `v`
if cell_size == elem_size:
return elem_size, 1, 0
per_cell = cell_data_size // elem_size
if per_cell == 1:
return cell_size, 1, 0
return elem_size, per_cell, cell_size - per_cell*elem_size
def cell_address(base, layout, index):
stride, per_cell, padding = layout
return base + index*stride + (index // per_cell)*padding
def map_capacity(data, length):
if data == 0:
return 0
capacity = 1 << (data & 63)
if length <= 0 or length > capacity:
return 0
if capacity > 1 << 40:
return 0
return capacity
def map_slots(data, length, key_layout, value_layout, hash_size, read_memory):
capacity = map_capacity(data, length)
if capacity == 0:
return
keys = data & ~63
values = cell_address(keys, key_layout, capacity)
hashes = cell_address(values, value_layout, capacity)
tombstone = 1 << (8*hash_size - 1)
found = 0
chunk = 1024
for start in range(0, capacity, chunk):
count = min(chunk, capacity - start)
raw = read_memory(hashes + start*hash_size, count*hash_size)
for j in range(count):
h = int.from_bytes(raw[j*hash_size:(j+1)*hash_size], "little")
if h != 0 and h & tombstone == 0:
yield cell_address(keys, key_layout, start+j), cell_address(values, value_layout, start+j)
found += 1
if found == length:
return
def array_count(data, length, capacity=None, read_memory=None):
# how many elements to show, none for a variable not yet initialised, whose data cannot be read
if data == 0:
return 0
if length <= 0:
return 0
if capacity is not None and length > capacity:
return 0
if read_memory is not None:
try:
read_memory(data, 1)
except Exception:
return 0
return length
def read_text(read_memory, address, count, unit, encoding):
if address == 0 or count <= 0:
return ""
size = min(count*unit, MAX_STRING_BYTES)
text = read_memory(address, size).decode(encoding, "replace")
if size < count*unit:
text += "..."
return text
################################################################################
# gdb
try:
import gdb
except ImportError:
gdb = None
if gdb is not None:
# NOTE: gdb 14 and later fetch the children of a `gdb.ValuePrinter` one at a time, through `num_children` and `child`
_GdbPrinter = getattr(gdb, "ValuePrinter", object)
class _GdbMapHintParameter(gdb.Parameter):
"""Whether Odin maps use the "map" display hint; when off, each entry is named by its key."""
set_doc = "Set whether Odin maps use the \"map\" display hint."
show_doc = "Show whether Odin maps use the \"map\" display hint."
def __init__(self):
super().__init__("odin-map-hint", gdb.COMMAND_DATA, gdb.PARAM_BOOLEAN)
self.value = True
try:
gdb.parameter("odin-map-hint")
except RuntimeError:
_gdb_map_hint_parameter = _GdbMapHintParameter()
def _gdb_map_hint():
try:
return bool(gdb.parameter("odin-map-hint"))
except RuntimeError:
return True
def _gdb_read(address, size):
return bytes(gdb.selected_inferior().read_memory(address, size))
def _gdb_fields(t):
return {f.name: f.type for f in t.fields()}
def _gdb_kind(t):
if t.code != gdb.TYPE_CODE_STRUCT:
return None
fields = t.fields()
names = [f.name for f in fields]
data_is_pointer = False
data_is_array = False
target_fields = []
if names and names[0] == "data":
data = fields[0].type.strip_typedefs()
data_is_pointer = data.code == gdb.TYPE_CODE_PTR
data_is_array = data.code == gdb.TYPE_CODE_ARRAY
if data_is_pointer:
target = data.target().strip_typedefs()
if target.code == gdb.TYPE_CODE_STRUCT:
target_fields = [f.name for f in target.fields()]
return odin_kind(t.name, names, data_is_pointer, data_is_array, target_fields)
def _gdb_cell_layout(elem, cell):
cell_data_size = 0
if cell.sizeof != elem.sizeof:
cell_data_size = _gdb_fields(cell.strip_typedefs())["v"].sizeof
return cell_layout(elem.sizeof, cell.sizeof, cell_data_size)
def _gdb_type(*names):
for name in names:
try:
return gdb.lookup_type(name)
except gdb.error:
continue
return None
def _gdb_odin_type(name):
basic = ODIN_BASIC_TYPES.get(name)
if basic is None:
return _gdb_type(name, "struct " + name, "union " + name, "enum " + name)
kind, size = basic
if size is None:
size = gdb.lookup_type("void").pointer().sizeof
if kind == "rawptr":
return gdb.lookup_type("void").pointer()
if kind == "cstring":
return gdb.lookup_type("char").pointer()
if kind == "cstring16":
return _gdb_type("char16_t", "unsigned short").pointer()
if kind == "float":
return _gdb_type({2: "_Float16", 4: "float", 8: "double"}[size])
if kind == "bool":
return _gdb_type("_Bool", "bool")
if kind == "rune":
return _gdb_type("char32_t", "unsigned int")
ints = {1: "char", 2: "short", 4: "int", 8: "long long", 16: "__int128"}
if kind == "uint":
return _gdb_type("unsigned " + ints[size])
return _gdb_type("signed char" if size == 1 else ints[size])
def _gdb_is_short(t):
t = t.strip_typedefs()
if t.code in (gdb.TYPE_CODE_INT, gdb.TYPE_CODE_ENUM, gdb.TYPE_CODE_FLT, gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_CHAR, gdb.TYPE_CODE_BOOL):
return True
return _gdb_kind(t) in ("string", "string16")
class _GdbString(_GdbPrinter):
def __init__(self, val, kind):
self._val = val
self._unit, self._encoding = (1, "utf-8") if kind == "string" else (2, "utf-16-le")
def to_string(self):
return read_text(_gdb_read, int(self._val["data"]), int(self._val["len"]), self._unit, self._encoding)
def display_hint(self):
return "string"
class _GdbArray(_GdbPrinter):
# A dynamic array also shows its allocator, after its elements
def __init__(self, val, kind):
self._val = val
self._kind = kind
self._data = val["data"]
length = int(val["len"])
if kind == "fixed":
capacity = self._data.type.strip_typedefs().range()[1] + 1
self._count = array_count(1, length, capacity)
else:
capacity = int(val["cap"]) if kind == "dynamic" else None
self._count = array_count(int(self._data), length, capacity, _gdb_read)
def to_string(self):
if self._kind == "dynamic":
return "len=%d cap=%d" % (int(self._val["len"]), int(self._val["cap"]))
return "len=%d" % int(self._val["len"])
def display_hint(self):
if self._kind == "dynamic":
return None
return "array"
def num_children(self):
if self._kind == "dynamic":
return self._count + 1
return self._count
def child(self, index):
if index == self._count:
return "allocator", self._val["allocator"]
if self._kind == "fixed":
return "[%d]" % index, self._data[index]
return "[%d]" % index, (self._data + index).dereference()
def children(self):
for i in range(self.num_children()):
yield self.child(i)
class _GdbSoa(_GdbPrinter):
# Each field as an array of the length, then a dynamic array's allocator
def __init__(self, val, kind):
self._val = val
self._kind = kind
t = val.type.strip_typedefs()
self._fields = soa_fields([f.name for f in t.fields()])
length = int(val["__$len"])
capacity = int(val["__$cap"]) if kind == "soa_dynamic" else None
first = int(val[self._fields[0]]) if self._fields else 0
self._count = array_count(first, length, capacity, _gdb_read)
def to_string(self):
if self._kind == "soa_dynamic":
return "len=%d cap=%d" % (int(self._val["__$len"]), int(self._val["__$cap"]))
return "len=%d" % int(self._val["__$len"])
def num_children(self):
fields = len(self._fields) if self._count > 0 else 0
if self._kind == "soa_dynamic":
return fields + 1
return fields
def child(self, index):
if self._count == 0 or index == len(self._fields):
return "allocator", self._val["allocator"]
name = self._fields[index]
pointer = self._val[name]
array = pointer.type.strip_typedefs().target().array(0, self._count - 1)
return name, pointer.cast(array.pointer()).dereference()
def children(self):
for i in range(self.num_children()):
yield self.child(i)
class _GdbMap(_GdbPrinter):
def __init__(self, val):
self._val = val
f = _gdb_fields(val["data"].type.strip_typedefs().target().strip_typedefs())
self._key_type = f["key"]
self._value_type = f.get("value")
key_layout = _gdb_cell_layout(f["key"], f["key_cell"])
value_layout = (0, 1, 0)
if self._value_type is not None:
value_layout = _gdb_cell_layout(self._value_type, f["value_cell"])
data = int(val["data"])
self._count = int(val["len"])
if map_capacity(data, self._count) == 0:
self._count = 0
self._slots = []
self._scan = map_slots(data, self._count, key_layout, value_layout, f["hash"].sizeof, _gdb_read)
self._set = self._value_type is None or self._value_type.sizeof == 0
self._hint = _gdb_map_hint()
self._pairs = self._hint or not _gdb_is_short(self._key_type)
def to_string(self):
return "len=%d" % int(self._val["len"])
def display_hint(self):
if self._set:
return "array"
if self._hint:
return "map"
return None
def _slot(self, entry):
while len(self._slots) <= entry:
try:
self._slots.append(next(self._scan))
except (StopIteration, gdb.MemoryError):
return None
return self._slots[entry]
def num_children(self):
if self._set or not self._pairs:
return self._count
return 2*self._count
def child(self, index):
entry = index
if not self._set and self._pairs:
entry = index//2
slot = self._slot(entry)
if slot is None:
return None
key = gdb.Value(slot[0]).cast(self._key_type.pointer()).dereference()
if self._set:
return "[%d]" % entry, key
value = gdb.Value(slot[1]).cast(self._value_type.pointer()).dereference()
if self._hint:
if index % 2 == 0:
return "key", key
return "value", value
if self._pairs:
if index % 2 == 0:
return "[%d].key" % entry, key
return "[%d].value" % entry, value
return "[%s]" % key.format_string(), value
def children(self):
for i in range(self.num_children()):
c = self.child(i)
if c is None:
return
yield c
class _GdbUnion(_GdbPrinter):
def __init__(self, val):
self._val = val
def to_string(self):
names = [f.name for f in self._val.type.strip_typedefs().fields()]
tag = int(self._val["tag"]) if "tag" in names else None
try:
name = union_variant(names, tag)
except ValueError as e:
return "<%s>" % e
if name is None or (tag is None and int(self._val[name]) == 0):
return "nil"
return self._val[name]
class _GdbAny(_GdbPrinter):
# The value it holds, after the name of its type; an unknown type shows the `any` itself
def __init__(self, val):
self._val = val
def to_string(self):
data = int(self._val["data"])
name = self._val["id"].format_string()
if data == 0 or int(self._val["id"]) == 0:
return "nil"
t = _gdb_odin_type(name)
if t is None:
return self._val.format_string(raw=True)
value = gdb.Value(data).cast(t.pointer()).dereference()
return "%s %s" % (name, value.format_string())
def _gdb_lookup(val):
t = val.type.strip_typedefs()
if t.code == gdb.TYPE_CODE_UNION and is_odin_union([f.name for f in t.fields()]):
return _GdbUnion(val)
kind = _gdb_kind(t)
if kind in ("string", "string16"):
return _GdbString(val, kind)
if kind == "any":
return _GdbAny(val)
if kind == "map":
return _GdbMap(val)
if kind in ("soa_slice", "soa_dynamic"):
return _GdbSoa(val, kind)
if kind is not None:
return _GdbArray(val, kind)
return None
# NOTE: loaded from a binary's `.debug_gdb_scripts` section, the printers belong to that binary
if gdb.current_objfile() is not None:
gdb.current_objfile().pretty_printers.append(_gdb_lookup)
else:
gdb.pretty_printers.append(_gdb_lookup)
################################################################################
# lldb
try:
import lldb
except ImportError:
lldb = None
if lldb is not None:
def _lldb_kind(sbtype):
t = sbtype.GetCanonicalType()
if t.GetTypeClass() not in (lldb.eTypeClassStruct, lldb.eTypeClassClass):
return None
names = [t.GetFieldAtIndex(i).GetName() for i in range(t.GetNumberOfFields())]
data_is_pointer = False
data_is_array = False
target_fields = []
if names and names[0] == "data":
data = t.GetFieldAtIndex(0).GetType().GetCanonicalType()
data_is_pointer = data.IsPointerType()
data_is_array = data.IsArrayType()
if data_is_pointer:
target = data.GetPointeeType().GetCanonicalType()
target_fields = [target.GetFieldAtIndex(i).GetName() for i in range(target.GetNumberOfFields())]
return odin_kind(t.GetName(), names, data_is_pointer, data_is_array, target_fields)
def _lldb_reader(valobj):
process = valobj.GetProcess()
def read(address, size):
error = lldb.SBError()
data = process.ReadMemory(address, size, error)
if error.Fail():
raise MemoryError(error.GetCString())
return data
return read
def _lldb_fields(t):
t = t.GetCanonicalType()
return {t.GetFieldAtIndex(i).GetName(): t.GetFieldAtIndex(i).GetType() for i in range(t.GetNumberOfFields())}
def _lldb_cell_layout(elem, cell):
cell_data_size = 0
if cell.GetByteSize() != elem.GetByteSize():
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
basic = ODIN_BASIC_TYPES.get(name)
if basic is None:
types = target.FindTypes(name)
for i in range(types.GetSize()):
t = types.GetTypeAtIndex(i)
if typeid_hash(t.GetName()) == id:
return t
return None
kind, size = basic
if size is None:
size = target.GetAddressByteSize()
if kind == "rawptr":
return target.GetBasicType(lldb.eBasicTypeVoid).GetPointerType()
if kind == "cstring":
return target.GetBasicType(lldb.eBasicTypeChar).GetPointerType()
if kind == "cstring16":
return target.GetBasicType(lldb.eBasicTypeChar16).GetPointerType()
basics = {
("int", 1): lldb.eBasicTypeSignedChar,
("int", 2): lldb.eBasicTypeShort,
("int", 4): lldb.eBasicTypeInt,
("int", 8): lldb.eBasicTypeLongLong,
("int", 16): lldb.eBasicTypeInt128,
("uint", 1): lldb.eBasicTypeUnsignedChar,
("uint", 2): lldb.eBasicTypeUnsignedShort,
("uint", 4): lldb.eBasicTypeUnsignedInt,
("uint", 8): lldb.eBasicTypeUnsignedLongLong,
("uint", 16): lldb.eBasicTypeUnsignedInt128,
("float", 2): lldb.eBasicTypeHalf,
("float", 4): lldb.eBasicTypeFloat,
("float", 8): lldb.eBasicTypeDouble,
("bool", 1): lldb.eBasicTypeBool,
("rune", 4): lldb.eBasicTypeChar32,
}
basic_type = basics.get((kind, size))
if basic_type is None:
return None
return target.GetBasicType(basic_type)
def lldb_is_string(sbtype, internal_dict):
return _lldb_kind(sbtype) in ("string", "string16")
def lldb_is_array(sbtype, internal_dict):
return _lldb_kind(sbtype) in ("slice", "dynamic", "fixed")
def lldb_is_soa(sbtype, internal_dict):
return _lldb_kind(sbtype) in ("soa_slice", "soa_dynamic")
def lldb_is_map(sbtype, internal_dict):
return _lldb_kind(sbtype) == "map"
def lldb_is_any(sbtype, internal_dict):
return _lldb_kind(sbtype) == "any"
def lldb_is_container(sbtype, internal_dict):
return _lldb_kind(sbtype) in ("slice", "dynamic", "fixed", "map", "soa_slice", "soa_dynamic")
def lldb_is_union(sbtype, internal_dict):
t = sbtype.GetCanonicalType()
return t.GetTypeClass() == lldb.eTypeClassUnion and is_odin_union(list(_lldb_fields(t)))
def _lldb_union_value(valobj):
# the variant the union holds, None when it is nil; raises ValueError for an invalid tag
v = valobj.GetNonSyntheticValue()
names = list(_lldb_fields(v.GetType()))
tag = v.GetChildMemberWithName("tag").GetValueAsUnsigned() if "tag" in names else None
name = union_variant(names, tag)
if name is None:
return None
value = v.GetChildMemberWithName(name)
if tag is None and value.GetValueAsUnsigned() == 0:
return None
value.SetPreferSyntheticValue(True)
return value
def _lldb_any_value(valobj):
# the name of the type and the value an `any` holds; the value is None when it is nil, and when its type is unknown
v = valobj.GetNonSyntheticValue()
data = v.GetChildMemberWithName("data").GetValueAsUnsigned()
id = v.GetChildMemberWithName("id")
name = id.GetValue() or ""
if data == 0 or id.GetValueAsUnsigned() == 0:
return "nil", None
t = _lldb_odin_type(valobj.GetTarget(), name, id.GetValueAsUnsigned())
if t is None:
return name, None
if name not in ODIN_BASIC_TYPES:
name = t.GetName()
value = valobj.CreateValueFromAddress("value", data, t)
value.SetPreferSyntheticValue(True)
return name, value
def lldb_union_summary(valobj, internal_dict):
try:
value = _lldb_union_value(valobj)
except ValueError as e:
return "<%s>" % e
if value is None:
return "nil"
# NOTE: a struct has neither, so it is shown by its fields, the union's children
return value.GetSummary() or value.GetValue() or ""
def lldb_any_summary(valobj, internal_dict):
name, value = _lldb_any_value(valobj)
if value is None:
return name
return ("%s %s" % (name, value.GetSummary() or value.GetValue() or "")).strip()
def lldb_string_summary(valobj, internal_dict):
v = valobj.GetNonSyntheticValue()
unit, encoding = (1, "utf-8") if _lldb_kind(v.GetType()) == "string" else (2, "utf-16-le")
data = v.GetChildMemberWithName("data").GetValueAsUnsigned()
count = v.GetChildMemberWithName("len").GetValueAsSigned()
try:
text = read_text(_lldb_reader(v), data, count, unit, encoding)
except MemoryError:
return "<invalid>"
return '"%s"' % text.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
def lldb_len_summary(valobj, internal_dict):
v = valobj.GetNonSyntheticValue()
kind = _lldb_kind(v.GetType())
if kind in ("soa_slice", "soa_dynamic"):
summary = "len=%d" % v.GetChildMemberWithName("__$len").GetValueAsSigned()
if kind == "soa_dynamic":
summary += " cap=%d" % v.GetChildMemberWithName("__$cap").GetValueAsSigned()
return summary
summary = "len=%d" % v.GetChildMemberWithName("len").GetValueAsSigned()
if kind == "dynamic":
summary += " cap=%d" % v.GetChildMemberWithName("cap").GetValueAsSigned()
return summary
class LldbArraySynth:
# A dynamic array also shows its allocator, after its elements
def __init__(self, valobj, internal_dict):
self.valobj = valobj
def update(self):
data = self.valobj.GetChildMemberWithName("data")
t = data.GetType().GetCanonicalType()
length = self.valobj.GetChildMemberWithName("len").GetValueAsSigned()
self.allocator = None
if t.IsArrayType():
self.elem = t.GetArrayElementType()
self.address = data.GetLoadAddress()
capacity = t.GetByteSize() // max(1, self.elem.GetByteSize())
self.count = array_count(1, length, capacity)
else:
self.elem = t.GetPointeeType()
self.address = data.GetValueAsUnsigned()
capacity = None
if self.valobj.GetChildMemberWithName("cap").IsValid():
capacity = self.valobj.GetChildMemberWithName("cap").GetValueAsSigned()
self.allocator = self.valobj.GetChildMemberWithName("allocator")
self.count = array_count(self.address, length, capacity, _lldb_reader(self.valobj))
return False
def num_children(self):
if self.allocator is not None:
return self.count + 1
return self.count
def get_child_index(self, name):
if name == "allocator" and self.allocator is not None:
return self.count
try:
return int(name.strip("[]"))
except ValueError:
return -1
def get_child_at_index(self, index):
if index == self.count:
return self.allocator
return self.valobj.CreateValueFromAddress("[%d]" % index, self.address + index*self.elem.GetByteSize(), self.elem)
def has_children(self):
return True
class LldbSoaSynth:
# Each field as an array of the length, then a dynamic array's allocator
def __init__(self, valobj, internal_dict):
self.valobj = valobj
def update(self):
v = self.valobj
kind = _lldb_kind(v.GetType())
self.fields = soa_fields(list(_lldb_fields(v.GetType())))
length = v.GetChildMemberWithName("__$len").GetValueAsSigned()
capacity = None
self.allocator = None
if kind == "soa_dynamic":
capacity = v.GetChildMemberWithName("__$cap").GetValueAsSigned()
self.allocator = v.GetChildMemberWithName("allocator")
first = v.GetChildMemberWithName(self.fields[0]).GetValueAsUnsigned() if self.fields else 0
self.count = array_count(first, length, capacity, _lldb_reader(v))
return False
def num_children(self):
fields = len(self.fields) if self.count > 0 else 0
if self.allocator is not None:
return fields + 1
return fields
def get_child_index(self, name):
fields = len(self.fields) if self.count > 0 else 0
if name == "allocator" and self.allocator is not None:
return fields
if name in self.fields[:fields]:
return self.fields.index(name)
return -1
def get_child_at_index(self, index):
if self.count == 0 or index == len(self.fields):
return self.allocator
name = self.fields[index]
pointer = self.valobj.GetChildMemberWithName(name)
array = pointer.GetType().GetCanonicalType().GetPointeeType().GetArrayType(self.count)
return self.valobj.CreateValueFromAddress(name, pointer.GetValueAsUnsigned(), array)
def has_children(self):
return True
class LldbMapSynth:
# Each entry is a child named by its key; a key without a short form, such as a struct,
# gives two children per entry instead, `[i].key` and `[i].value`; a map of zero sized values shows its keys
def __init__(self, valobj, internal_dict):
self.valobj = valobj
def update(self):
data = self.valobj.GetChildMemberWithName("data")
f = _lldb_fields(data.GetType().GetCanonicalType().GetPointeeType())
self.key_type = f["key"]
self.value_type = f.get("value")
key_layout = _lldb_cell_layout(f["key"], f["key_cell"])
value_layout = (0, 1, 0)
if self.value_type is not None:
value_layout = _lldb_cell_layout(self.value_type, f["value_cell"])
raw = data.GetValueAsUnsigned()
self.count = self.valobj.GetChildMemberWithName("len").GetValueAsSigned()
if map_capacity(raw, self.count) == 0:
self.count = 0
self.slots = []
self.scan = map_slots(raw, self.count, key_layout, value_layout, f["hash"].GetByteSize(), _lldb_reader(self.valobj))
self.set = self.value_type is None or self.value_type.GetByteSize() == 0
key = self.key_type.GetCanonicalType()
self.pairs = not (self.set or key.GetTypeClass() in (lldb.eTypeClassBuiltin, lldb.eTypeClassEnumeration, lldb.eTypeClassPointer) or lldb_is_string(key, None))
return False
def _slot(self, entry):
while len(self.slots) <= entry:
try:
self.slots.append(next(self.scan))
except (StopIteration, MemoryError):
return None
return self.slots[entry]
def num_children(self):
return 2*self.count if self.pairs else self.count
def get_child_index(self, name):
return -1
def get_child_at_index(self, index):
entry = index//2 if self.pairs else index
slot = self._slot(entry)
if slot is None:
return None
key, value = slot
if self.set:
return self.valobj.CreateValueFromAddress("[%d]" % entry, key, self.key_type)
if self.pairs:
if index % 2 == 0:
return self.valobj.CreateValueFromAddress("[%d].key" % entry, key, self.key_type)
return self.valobj.CreateValueFromAddress("[%d].value" % entry, value, self.value_type)
k = self.valobj.CreateValueFromAddress("key", key, self.key_type)
text = k.GetSummary() or k.GetValue() or str(entry)
return self.valobj.CreateValueFromAddress("[%s]" % text, value, self.value_type)
def has_children(self):
return True
class LldbUnionSynth:
# The children of the variant the union holds
def __init__(self, valobj, internal_dict):
self.valobj = valobj
def update(self):
try:
self.value = _lldb_union_value(self.valobj)
except ValueError:
self.value = None
if self.value is not None and self.value.GetSummary() and not self.value.IsSynthetic():
# a string, which its summary shows whole
self.value = None
return False
def num_children(self):
return self.value.GetNumChildren() if self.value is not None else 0
def get_child_index(self, name):
return self.value.GetIndexOfChildWithName(name) if self.value is not None else -1
def get_child_at_index(self, index):
return self.value.GetChildAtIndex(index)
def has_children(self):
return self.value is not None and self.value.MightHaveChildren()
class LldbAnySynth:
# The children of the value it holds, or of the `any` itself when that value's type is unknown
def __init__(self, valobj, internal_dict):
self.valobj = valobj
def update(self):
name, value = _lldb_any_value(self.valobj)
self.value = value
if value is None and name != "nil":
self.value = self.valobj.GetNonSyntheticValue()
if self.value is not None and self.value.GetSummary() and not self.value.IsSynthetic():
# a string, which its summary shows whole
self.value = None
return False
def num_children(self):
return self.value.GetNumChildren() if self.value is not None else 0
def get_child_index(self, name):
return self.value.GetIndexOfChildWithName(name) if self.value is not None else -1
def get_child_at_index(self, index):
return self.value.GetChildAtIndex(index)
def has_children(self):
return self.value is not None and self.value.MightHaveChildren()
def __lldb_init_module(debugger, internal_dict):
# NOTE: commands go through the interpreter with a result object, as `SBDebugger.HandleCommand` writes to
# the debugger's output, which lldb-dap uses for its protocol, so editors using it lose the session.
# `--recognizer-function` needs LLDB 15; before it, types are matched by their names, so named types are not.
m = __name__
rules = [
("type summary add -w odin -F %s.lldb_string_summary" % m, "lldb_is_string", "^string(16)?$"),
("type summary add -w odin -e -F %s.lldb_len_summary" % m, "lldb_is_container", "^([[][]]|[[]dynamic|map[[]|#soa)"),
("type synthetic add -w odin -l %s.LldbArraySynth" % m, "lldb_is_array", "^([[][]]|[[]dynamic)"),
("type synthetic add -w odin -l %s.LldbSoaSynth" % m, "lldb_is_soa", "^#soa[[]([]]|dynamic)"),
("type synthetic add -w odin -l %s.LldbMapSynth" % m, "lldb_is_map", "^map[[]"),
("type summary add -w odin -e -F %s.lldb_union_summary" % m, "lldb_is_union", "^union[{]"),
("type synthetic add -w odin -l %s.LldbUnionSynth" % m, "lldb_is_union", "^union[{]"),
("type summary add -w odin -e -F %s.lldb_any_summary" % m, "lldb_is_any", "^any$"),
("type synthetic add -w odin -l %s.LldbAnySynth" % m, "lldb_is_any", "^any$"),
]
interpreter = debugger.GetCommandInterpreter()
def run(command):
result = lldb.SBCommandReturnObject()
interpreter.HandleCommand(command, result)
return result.Succeeded()
for command, recognizer, regex in rules:
if not run("%s --recognizer-function %s.%s" % (command, m, recognizer)):
run('%s -x "%s"' % (command, regex))
run("type category enable odin")