toolbox 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/ruby/gdb.rb +135 -0
  4. data/bake/toolbox/gdb.rb +137 -0
  5. data/bake/toolbox/lldb.rb +137 -0
  6. data/context/fiber-debugging.md +171 -0
  7. data/context/getting-started.md +200 -0
  8. data/context/heap-debugging.md +351 -0
  9. data/context/index.yaml +28 -0
  10. data/context/object-inspection.md +208 -0
  11. data/context/stack-inspection.md +188 -0
  12. data/data/toolbox/command.py +479 -0
  13. data/data/toolbox/constants.py +200 -0
  14. data/data/toolbox/context.py +371 -0
  15. data/data/toolbox/debugger/__init__.py +101 -0
  16. data/data/toolbox/debugger/gdb_backend.py +664 -0
  17. data/data/toolbox/debugger/lldb_backend.py +986 -0
  18. data/data/toolbox/fiber.py +877 -0
  19. data/data/toolbox/format.py +205 -0
  20. data/data/toolbox/heap.py +679 -0
  21. data/data/toolbox/init.py +89 -0
  22. data/data/toolbox/print.py +79 -0
  23. data/data/toolbox/rarray.py +116 -0
  24. data/data/toolbox/rbasic.py +99 -0
  25. data/data/toolbox/rbignum.py +48 -0
  26. data/data/toolbox/rclass.py +136 -0
  27. data/data/toolbox/readme.md +214 -0
  28. data/data/toolbox/rexception.py +150 -0
  29. data/data/toolbox/rfloat.py +88 -0
  30. data/data/toolbox/rhash.py +151 -0
  31. data/data/toolbox/rstring.py +230 -0
  32. data/data/toolbox/rstruct.py +149 -0
  33. data/data/toolbox/rsymbol.py +278 -0
  34. data/data/toolbox/rvalue.py +183 -0
  35. data/data/toolbox/stack.py +620 -0
  36. data/lib/toolbox/gdb.rb +21 -0
  37. data/lib/toolbox/lldb.rb +21 -0
  38. data/lib/toolbox/version.rb +7 -1
  39. data/lib/toolbox.rb +9 -24
  40. data/license.md +21 -0
  41. data/readme.md +64 -0
  42. data/releases.md +9 -0
  43. data.tar.gz.sig +0 -0
  44. metadata +95 -165
  45. metadata.gz.sig +0 -0
  46. data/Rakefile +0 -61
  47. data/lib/dirs.rb +0 -9
  48. data/lib/toolbox/config.rb +0 -211
  49. data/lib/toolbox/default_controller.rb +0 -393
  50. data/lib/toolbox/helpers.rb +0 -11
  51. data/lib/toolbox/rendering.rb +0 -413
  52. data/lib/toolbox/searching.rb +0 -85
  53. data/lib/toolbox/session_params.rb +0 -63
  54. data/lib/toolbox/sorting.rb +0 -74
  55. data/locale/de/LC_MESSAGES/toolbox.mo +0 -0
  56. data/public/images/add.png +0 -0
  57. data/public/images/arrow_down.gif +0 -0
  58. data/public/images/arrow_up.gif +0 -0
  59. data/public/images/close.png +0 -0
  60. data/public/images/edit.gif +0 -0
  61. data/public/images/email.png +0 -0
  62. data/public/images/page.png +0 -0
  63. data/public/images/page_acrobat.png +0 -0
  64. data/public/images/page_add.png +0 -0
  65. data/public/images/page_copy.png +0 -0
  66. data/public/images/page_delete.png +0 -0
  67. data/public/images/page_edit.png +0 -0
  68. data/public/images/page_excel.png +0 -0
  69. data/public/images/page_list.png +0 -0
  70. data/public/images/page_save.png +0 -0
  71. data/public/images/page_word.png +0 -0
  72. data/public/images/remove.png +0 -0
  73. data/public/images/show.gif +0 -0
  74. data/public/images/spinner.gif +0 -0
  75. data/public/javascripts/popup.js +0 -498
  76. data/public/javascripts/toolbox.js +0 -18
  77. data/public/stylesheets/context_menu.css +0 -168
  78. data/public/stylesheets/popup.css +0 -30
  79. data/public/stylesheets/toolbox.css +0 -107
  80. data/view/toolbox/_collection.html.erb +0 -24
  81. data/view/toolbox/_collection_header.html.erb +0 -7
  82. data/view/toolbox/_context_menu.html.erb +0 -17
  83. data/view/toolbox/_dialogs.html.erb +0 -6
  84. data/view/toolbox/_form.html.erb +0 -30
  85. data/view/toolbox/_form_collection_row.html.erb +0 -18
  86. data/view/toolbox/_form_fieldset.html.erb +0 -30
  87. data/view/toolbox/_form_fieldset_row.html.erb +0 -19
  88. data/view/toolbox/_list.html.erb +0 -25
  89. data/view/toolbox/_list_row.html.erb +0 -10
  90. data/view/toolbox/_menu.html.erb +0 -7
  91. data/view/toolbox/_search_field.html.erb +0 -8
  92. data/view/toolbox/_show.html.erb +0 -12
  93. data/view/toolbox/_show_collection_row.html.erb +0 -6
  94. data/view/toolbox/_show_fieldset.html.erb +0 -21
  95. data/view/toolbox/edit.html.erb +0 -5
  96. data/view/toolbox/index.html.erb +0 -3
  97. data/view/toolbox/new.html.erb +0 -9
  98. data/view/toolbox/show.html.erb +0 -39
@@ -0,0 +1,89 @@
1
+ """
2
+ Ruby Toolbox - Unified entry point for GDB and LLDB
3
+
4
+ This module auto-detects which debugger is running and loads
5
+ the appropriate Ruby debugging extensions.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+
11
+ # Get the directory containing this file (data/toolbox)
12
+ toolbox_dir = os.path.dirname(os.path.abspath(__file__))
13
+
14
+ # Add to Python path for imports
15
+ if toolbox_dir not in sys.path:
16
+ sys.path.insert(0, toolbox_dir)
17
+
18
+ # Load debugger abstraction (auto-detects GDB or LLDB)
19
+ import debugger
20
+
21
+ # Load Ruby debugging extensions
22
+ loaded_extensions = []
23
+ failed_extensions = []
24
+
25
+ # Try to load each extension individually
26
+ extensions_to_load = [
27
+ ('print', 'rb-print'),
28
+ ('context', 'rb-context, rb-context-storage'),
29
+ ('fiber', 'rb-fiber-scan-heap, rb-fiber-switch'),
30
+ ('stack', 'rb-stack-trace'),
31
+ ('heap', 'rb-heap-scan'),
32
+ ]
33
+
34
+ for module_name, commands in extensions_to_load:
35
+ try:
36
+ if module_name == 'print':
37
+ import print as print_module
38
+ elif module_name == 'context':
39
+ import context
40
+ elif module_name == 'fiber':
41
+ import fiber
42
+ elif module_name == 'stack':
43
+ import stack
44
+ elif module_name == 'heap':
45
+ import heap
46
+ loaded_extensions.append((module_name, commands))
47
+ except Exception as e:
48
+ # Catch all exceptions during module load
49
+ failed_extensions.append((module_name, str(e)))
50
+ import traceback
51
+ print(f"Failed to load {module_name}: {e}", file=sys.stderr)
52
+ traceback.print_exc(file=sys.stderr)
53
+
54
+ # Silently load - no status messages printed by default
55
+ # Users can run 'help' to see available commands
56
+
57
+ # For LLDB, register commands that were successfully loaded
58
+ if debugger.DEBUGGER_NAME == 'lldb':
59
+ import lldb
60
+
61
+ # Get all registered commands
62
+ for cmd_name, cmd_obj in debugger.Command._commands.items():
63
+ # Create a wrapper function in this module's namespace
64
+ func_name = f"_cmd_{cmd_name.replace('-', '_')}"
65
+
66
+ # Create closure that captures cmd_obj
67
+ def make_wrapper(command_obj):
68
+ def wrapper(debugger_obj, command, result, internal_dict):
69
+ try:
70
+ # Check if stdout is a TTY
71
+ from_tty = sys.stdout.isatty()
72
+ command_obj.invoke(command, from_tty=from_tty)
73
+ except Exception as e:
74
+ print(f"Error: {e}")
75
+ import traceback
76
+ traceback.print_exc()
77
+ return wrapper
78
+
79
+ # Add to this module's globals
80
+ globals()[func_name] = make_wrapper(cmd_obj)
81
+
82
+ # Register with LLDB
83
+ # The module is imported as 'init' by LLDB
84
+ result = lldb.SBCommandReturnObject()
85
+ cmd_str = f"command script add -f init.{func_name} {cmd_name}"
86
+ lldb.debugger.GetCommandInterpreter().HandleCommand(cmd_str, result)
87
+
88
+ # Silently ignore registration failures
89
+
@@ -0,0 +1,79 @@
1
+ """Print command for Ruby values."""
2
+
3
+ import debugger
4
+ import sys
5
+
6
+ # Import utilities
7
+ import command
8
+ import constants
9
+ import rvalue
10
+ import rstring
11
+ import rarray
12
+ import rhash
13
+ import rsymbol
14
+ import rstruct
15
+ import rfloat
16
+ import rbignum
17
+ import rbasic
18
+ import format
19
+
20
+
21
+ class RubyObjectPrinter:
22
+ """Print Ruby objects with recursive descent into nested structures."""
23
+
24
+ USAGE = command.Usage(
25
+ summary="Print Ruby objects with recursive inspection",
26
+ parameters=[('value', 'VALUE or expression to print')],
27
+ options={
28
+ 'depth': (int, 1, 'Maximum recursion depth for nested objects')
29
+ },
30
+ flags=[
31
+ ('debug', 'Show internal structure and debug information')
32
+ ],
33
+ examples=[
34
+ ("rb-print $errinfo", "Print exception object"),
35
+ ("rb-print $ec->storage --depth 3", "Print fiber storage with depth 3"),
36
+ ("rb-print $ec->cfp->sp[-1] --debug", "Print top of stack with debug info")
37
+ ]
38
+ )
39
+
40
+ def invoke(self, arguments, terminal):
41
+ """Execute the print command.
42
+
43
+ Args:
44
+ arguments: Parsed Arguments object
45
+ terminal: Terminal formatter (already configured for TTY/non-TTY)
46
+ """
47
+ # Get options
48
+ max_depth = arguments.get_option('depth', 1)
49
+ debug_mode = arguments.has_flag('debug')
50
+
51
+ # Validate depth
52
+ if max_depth < 1:
53
+ print("Error: --depth must be >= 1")
54
+ return
55
+
56
+ # Create printer
57
+ printer = format.Printer(terminal, max_depth, debug_mode)
58
+
59
+ # Process each expression
60
+ for expression in arguments.expressions:
61
+ try:
62
+ # Evaluate the expression
63
+ ruby_value = debugger.parse_and_eval(expression)
64
+
65
+ # Interpret the value and let it print itself recursively
66
+ ruby_object = rvalue.interpret(ruby_value)
67
+ ruby_object.print_recursive(printer, max_depth)
68
+ except debugger.Error as e:
69
+ print(f"Error evaluating expression '{expression}': {e}")
70
+ except Exception as e:
71
+ print(f"Error processing '{expression}': {type(e).__name__}: {e}")
72
+ if debug_mode:
73
+ import traceback
74
+ traceback.print_exc(file=sys.stderr)
75
+
76
+
77
+ # Register command using new interface
78
+ debugger.register("rb-print", RubyObjectPrinter, usage=RubyObjectPrinter.USAGE)
79
+
@@ -0,0 +1,116 @@
1
+ import debugger
2
+ import rbasic
3
+ import constants
4
+ import format
5
+
6
+ class RArrayBase:
7
+ """Base class for RArray variants."""
8
+
9
+ def __init__(self, value):
10
+ """value is a VALUE pointing to a T_ARRAY object."""
11
+ self.value = value
12
+ self.rarray = value.cast(constants.type_struct('struct RArray').pointer())
13
+ self.basic = value.cast(constants.type_struct('struct RBasic').pointer())
14
+ self.flags = int(self.basic.dereference()['flags'])
15
+
16
+ def length(self):
17
+ """Get array length. Must be implemented by subclasses."""
18
+ raise NotImplementedError
19
+
20
+ def items_ptr(self):
21
+ """Get pointer to array items. Must be implemented by subclasses."""
22
+ raise NotImplementedError
23
+
24
+ def get_item(self, index):
25
+ """Get item at index."""
26
+ if index < 0 or index >= self.length():
27
+ raise IndexError(f"Index {index} out of range")
28
+ items = self.items_ptr()
29
+ return items[index]
30
+
31
+ def __len__(self):
32
+ """Support len() function."""
33
+ return self.length()
34
+
35
+ def __getitem__(self, index):
36
+ """Support indexing."""
37
+ return self.get_item(index)
38
+
39
+ def print_recursive(self, printer, depth):
40
+ """Print this array recursively."""
41
+ # Print the array header
42
+ printer.print(self)
43
+
44
+ # If depth is 0, don't recurse into elements
45
+ if depth <= 0:
46
+ if len(self) > 0:
47
+ printer.print_with_indent(printer.max_depth - depth, " ...")
48
+ return
49
+
50
+ # Print each element
51
+ for i in range(len(self)):
52
+ printer.print_item_label(printer.max_depth - depth, i)
53
+ try:
54
+ element = self[i]
55
+ printer.print_value(element, depth - 1)
56
+ except Exception as e:
57
+ print(f"Error accessing element {i}: {e}")
58
+
59
+ class RArrayEmbedded(RArrayBase):
60
+ """Embedded array (small arrays stored directly in struct)."""
61
+
62
+ def length(self):
63
+ # Extract length from flags using Ruby's encoding
64
+ # Length is stored in RUBY_FL_USER3|RUBY_FL_USER4 bits, shifted by RUBY_FL_USHIFT+3
65
+ mask = constants.flag("RUBY_FL_USER3") | constants.flag("RUBY_FL_USER4")
66
+ shift = constants.flag("RUBY_FL_USHIFT") + 3
67
+ return (self.flags & mask) >> shift
68
+
69
+ def items_ptr(self):
70
+ return self.rarray.dereference()['as']['ary']
71
+
72
+ def __str__(self):
73
+ """Return string representation of array."""
74
+ addr = int(self.value)
75
+ return f"<T_ARRAY@0x{addr:x} embedded length={len(self)}>"
76
+
77
+ def print_to(self, terminal):
78
+ """Print this array with formatting."""
79
+ addr = int(self.value)
80
+ details = f"embedded length={len(self)}"
81
+ terminal.print_type_tag('T_ARRAY', addr, details)
82
+
83
+ class RArrayHeap(RArrayBase):
84
+ """Heap array (larger arrays with separate memory allocation)."""
85
+
86
+ def length(self):
87
+ return int(self.rarray.dereference()['as']['heap']['len'])
88
+
89
+ def items_ptr(self):
90
+ return self.rarray.dereference()['as']['heap']['ptr']
91
+
92
+ def __str__(self):
93
+ """Return string representation of array."""
94
+ addr = int(self.value)
95
+ return f"<T_ARRAY@0x{addr:x} heap length={len(self)}>"
96
+
97
+ def print_to(self, terminal):
98
+ """Print this array with formatting."""
99
+ addr = int(self.value)
100
+ details = f"heap length={len(self)}"
101
+ terminal.print_type_tag('T_ARRAY', addr, details)
102
+
103
+ def RArray(value):
104
+ """Factory function that returns the appropriate RArray variant.
105
+
106
+ Caller should ensure value is a RUBY_T_ARRAY before calling this function.
107
+ """
108
+ # Get flags to determine embedded vs heap
109
+ basic = value.cast(constants.type_struct('struct RBasic').pointer())
110
+ flags = int(basic.dereference()['flags'])
111
+
112
+ # Check if array is embedded or heap-allocated using flags
113
+ if (flags & constants.get("RARRAY_EMBED_FLAG")) != 0:
114
+ return RArrayEmbedded(value)
115
+ else:
116
+ return RArrayHeap(value)
@@ -0,0 +1,99 @@
1
+ import debugger
2
+ import constants
3
+ import format
4
+
5
+ def type_of(value):
6
+ """Get the Ruby type of a VALUE.
7
+
8
+ Returns the RUBY_T_* constant value (e.g., RUBY_T_STRING, RUBY_T_ARRAY),
9
+ or None if the type cannot be determined.
10
+ """
11
+ basic = value.cast(constants.type_struct('struct RBasic').pointer())
12
+ flags = int(basic.dereference()['flags'])
13
+ RUBY_T_MASK = constants.type('RUBY_T_MASK')
14
+ return flags & RUBY_T_MASK
15
+
16
+ def is_type(value, ruby_type_constant):
17
+ """Check if a VALUE is of a specific Ruby type.
18
+
19
+ Arguments:
20
+ value: The GDB value to check
21
+ ruby_type_constant: String name of the constant (e.g., 'RUBY_T_STRING')
22
+
23
+ Returns:
24
+ True if the value is of the specified type, False otherwise
25
+ """
26
+ type_flag = type_of(value)
27
+ expected_type = constants.get(ruby_type_constant)
28
+ return type_flag == expected_type
29
+
30
+ # Map of type constants to their names for display
31
+ TYPE_NAMES = {
32
+ 'RUBY_T_NONE': 'T_NONE',
33
+ 'RUBY_T_OBJECT': 'T_OBJECT',
34
+ 'RUBY_T_CLASS': 'T_CLASS',
35
+ 'RUBY_T_MODULE': 'T_MODULE',
36
+ 'RUBY_T_FLOAT': 'T_FLOAT',
37
+ 'RUBY_T_STRING': 'T_STRING',
38
+ 'RUBY_T_REGEXP': 'T_REGEXP',
39
+ 'RUBY_T_ARRAY': 'T_ARRAY',
40
+ 'RUBY_T_HASH': 'T_HASH',
41
+ 'RUBY_T_STRUCT': 'T_STRUCT',
42
+ 'RUBY_T_BIGNUM': 'T_BIGNUM',
43
+ 'RUBY_T_FILE': 'T_FILE',
44
+ 'RUBY_T_DATA': 'T_DATA',
45
+ 'RUBY_T_MATCH': 'T_MATCH',
46
+ 'RUBY_T_COMPLEX': 'T_COMPLEX',
47
+ 'RUBY_T_RATIONAL': 'T_RATIONAL',
48
+ 'RUBY_T_NIL': 'T_NIL',
49
+ 'RUBY_T_TRUE': 'T_TRUE',
50
+ 'RUBY_T_FALSE': 'T_FALSE',
51
+ 'RUBY_T_SYMBOL': 'T_SYMBOL',
52
+ 'RUBY_T_FIXNUM': 'T_FIXNUM',
53
+ 'RUBY_T_UNDEF': 'T_UNDEF',
54
+ 'RUBY_T_IMEMO': 'T_IMEMO',
55
+ 'RUBY_T_NODE': 'T_NODE',
56
+ 'RUBY_T_ICLASS': 'T_ICLASS',
57
+ 'RUBY_T_ZOMBIE': 'T_ZOMBIE',
58
+ }
59
+
60
+ def type_name(value):
61
+ """Get the human-readable type name for a VALUE.
62
+
63
+ Returns:
64
+ String like 'T_STRING', 'T_ARRAY', 'T_HASH', etc., or 'Unknown(0x...)'
65
+ """
66
+ type_flag = type_of(value)
67
+
68
+ # Try to find matching type name
69
+ for const_name, display_name in TYPE_NAMES.items():
70
+ if constants.get(const_name) == type_flag:
71
+ return display_name
72
+
73
+ return f'Unknown(0x{type_flag:x})'
74
+
75
+ class RBasic:
76
+ """Generic Ruby object wrapper for unhandled types.
77
+
78
+ This provides a fallback for types that don't have specialized handlers.
79
+ """
80
+ def __init__(self, value):
81
+ self.value = value
82
+ self.basic = value.cast(constants.type_struct('struct RBasic').pointer())
83
+ self.flags = int(self.basic.dereference()['flags'])
84
+ self.type_flag = self.flags & constants.type('RUBY_T_MASK')
85
+
86
+ def __str__(self):
87
+ type_str = type_name(self.value)
88
+ return f"<{type_str}@0x{int(self.value):x}>"
89
+
90
+ def print_to(self, terminal):
91
+ """Print formatted basic object representation."""
92
+ type_str = type_name(self.value)
93
+ addr = int(self.value)
94
+ # Use print_type_tag for consistency with other types
95
+ terminal.print_type_tag(type_str, addr)
96
+
97
+ def print_recursive(self, printer, depth):
98
+ """Print this basic object (no recursion)."""
99
+ printer.print(self)
@@ -0,0 +1,48 @@
1
+ import debugger
2
+ import constants
3
+ import rbasic
4
+ import format
5
+
6
+ class RBignumObject:
7
+ def __init__(self, value):
8
+ self.value = value
9
+ self.rbignum = value.cast(constants.type_struct('struct RBignum').pointer())
10
+ self.basic = value.cast(constants.type_struct('struct RBasic').pointer())
11
+ self.flags = int(self.basic.dereference()['flags'])
12
+
13
+ def is_embedded(self):
14
+ # Check if FL_USER1 flag is set (RBIGNUM_EMBED_FLAG)
15
+ FL_USER1 = 1 << (constants.flag('RUBY_FL_USHIFT') + 1)
16
+ return bool(self.flags & FL_USER1)
17
+
18
+ def __len__(self):
19
+ if self.is_embedded():
20
+ # Embedded length is stored in flags
21
+ # Extract length from FL_USER2 onwards
22
+ return (self.flags >> (constants.flag('RUBY_FL_USHIFT') + 2)) & 0x1F
23
+ else:
24
+ return int(self.rbignum.dereference()['as']['heap']['len'])
25
+
26
+ def __str__(self):
27
+ addr = int(self.value)
28
+ if self.is_embedded():
29
+ return f"<T_BIGNUM@0x{addr:x} embedded length={len(self)}>"
30
+ else:
31
+ return f"<T_BIGNUM@0x{addr:x} heap length={len(self)}>"
32
+
33
+ def print_to(self, terminal):
34
+ """Print formatted bignum representation."""
35
+ addr = int(self.value)
36
+ storage = "embedded" if self.is_embedded() else "heap"
37
+ details = f"{storage} length={len(self)}"
38
+ terminal.print_type_tag('T_BIGNUM', addr, details)
39
+
40
+ def print_recursive(self, printer, depth):
41
+ """Print this bignum (no recursion needed)."""
42
+ printer.print(self)
43
+
44
+ def RBignum(value):
45
+ if rbasic.is_type(value, 'RUBY_T_BIGNUM'):
46
+ return RBignumObject(value)
47
+
48
+ return None
@@ -0,0 +1,136 @@
1
+ import debugger
2
+ import constants
3
+ import rvalue
4
+ import rstring
5
+
6
+ class RClass:
7
+ """Wrapper for Ruby class objects (klass pointers)."""
8
+
9
+ def __init__(self, klass_value):
10
+ """Initialize with a klass VALUE.
11
+
12
+ Args:
13
+ klass_value: A GDB value representing a Ruby class (klass pointer)
14
+ """
15
+ self.klass = klass_value
16
+ self._name = None
17
+
18
+ def name(self):
19
+ """Get the class name as a string.
20
+
21
+ Returns:
22
+ Class name string, or formatted anonymous class representation
23
+ """
24
+ if self._name is None:
25
+ self._name = self._get_class_name()
26
+ return self._name
27
+
28
+ def _get_class_name(self):
29
+ """Extract class name from klass pointer.
30
+
31
+ Tries multiple strategies across Ruby versions:
32
+ 1. Check against well-known global class pointers (rb_eStandardError, etc.)
33
+ 2. Try rb_classext_struct.classpath (Ruby 3.4+)
34
+ 3. Fall back to anonymous class format
35
+
36
+ Returns:
37
+ Class name string
38
+ """
39
+ try:
40
+ # Strategy 1: Check well-known exception classes
41
+ # This works in core dumps since we're just comparing pointers
42
+ well_known = [
43
+ ('rb_eException', 'Exception'),
44
+ ('rb_eStandardError', 'StandardError'),
45
+ ('rb_eSystemExit', 'SystemExit'),
46
+ ('rb_eInterrupt', 'Interrupt'),
47
+ ('rb_eSignal', 'SignalException'),
48
+ ('rb_eFatal', 'fatal'),
49
+ ('rb_eScriptError', 'ScriptError'),
50
+ ('rb_eLoadError', 'LoadError'),
51
+ ('rb_eNotImpError', 'NotImplementedError'),
52
+ ('rb_eSyntaxError', 'SyntaxError'),
53
+ ('rb_eSecurityError', 'SecurityError'),
54
+ ('rb_eNoMemError', 'NoMemoryError'),
55
+ ('rb_eTypeError', 'TypeError'),
56
+ ('rb_eArgError', 'ArgumentError'),
57
+ ('rb_eIndexError', 'IndexError'),
58
+ ('rb_eKeyError', 'KeyError'),
59
+ ('rb_eRangeError', 'RangeError'),
60
+ ('rb_eNameError', 'NameError'),
61
+ ('rb_eNoMethodError', 'NoMethodError'),
62
+ ('rb_eRuntimeError', 'RuntimeError'),
63
+ ('rb_eFrozenError', 'FrozenError'),
64
+ ('rb_eIOError', 'IOError'),
65
+ ('rb_eEOFError', 'EOFError'),
66
+ ('rb_eLocalJumpError', 'LocalJumpError'),
67
+ ('rb_eSysStackError', 'SystemStackError'),
68
+ ('rb_eRegexpError', 'RegexpError'),
69
+ ('rb_eThreadError', 'ThreadError'),
70
+ ('rb_eZeroDivError', 'ZeroDivisionError'),
71
+ ('rb_eFloatDomainError', 'FloatDomainError'),
72
+ ('rb_eStopIteration', 'StopIteration'),
73
+ ('rb_eMathDomainError', 'Math::DomainError'),
74
+ ('rb_eEncCompatError', 'Encoding::CompatibilityError'),
75
+ ]
76
+
77
+ klass_addr = int(self.klass)
78
+ for var_name, class_name in well_known:
79
+ try:
80
+ known_klass = debugger.parse_and_eval(var_name)
81
+ if int(known_klass) == klass_addr:
82
+ return class_name
83
+ except:
84
+ # Variable might not exist in this Ruby version
85
+ continue
86
+
87
+ # Strategy 2: Try modern rb_classext_struct.classpath (Ruby 3.4+)
88
+ try:
89
+ rclass = self.klass.cast(constants.type_struct('struct RClass').pointer())
90
+ # Try to access classext.classpath
91
+ try:
92
+ # Try embedded classext (RCLASS_EXT_EMBEDDED)
93
+ rclass_size = debugger.parse_and_eval("sizeof(struct RClass)")
94
+ classext_addr = int(self.klass) + int(rclass_size)
95
+ classext_type = constants.type_struct('rb_classext_t')
96
+ classext_ptr = debugger.create_value_from_address(classext_addr, classext_type).address
97
+ classpath_val = classext_ptr['classpath']
98
+ except:
99
+ # Try pointer-based classext
100
+ try:
101
+ classext_ptr = rclass['ptr']
102
+ classpath_val = classext_ptr['classpath']
103
+ except:
104
+ classpath_val = None
105
+
106
+ if classpath_val and int(classpath_val) != 0 and not rvalue.is_nil(classpath_val):
107
+ # Decode the classpath string
108
+ class_name_obj = rvalue.interpret(classpath_val)
109
+ if hasattr(class_name_obj, 'to_str'):
110
+ class_name = class_name_obj.to_str()
111
+ if class_name and not class_name.startswith('<'):
112
+ return class_name
113
+ except:
114
+ pass
115
+
116
+ # Strategy 3: Fall back to anonymous class format
117
+ return f"#<Class:0x{int(self.klass):x}>"
118
+ except Exception as e:
119
+ # Ultimate fallback
120
+ return f"#<Class:0x{int(self.klass):x}>"
121
+
122
+ def __str__(self):
123
+ """Return the class name."""
124
+ return self.name()
125
+
126
+ def get_class_name(klass_value):
127
+ """Get the name of a class from its klass pointer.
128
+
129
+ Args:
130
+ klass_value: A GDB value representing a Ruby class (klass pointer)
131
+
132
+ Returns:
133
+ Class name string
134
+ """
135
+ rc = RClass(klass_value)
136
+ return rc.name()