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,214 @@
1
+ # Ruby Toolbox Extensions
2
+
3
+ This directory contains Ruby debugging extensions that work with both GDB and LLDB through automatic debugger detection.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ data/toolbox/
9
+ ├── init.py # Single entry point for both debuggers
10
+ ├── debugger.py # Auto-detects GDB or LLDB
11
+ ├── debugger/ # Abstraction layer implementations
12
+ │ ├── gdb.py # GDB backend
13
+ │ ├── lldb.py # LLDB backend
14
+ │ └── *.md # Documentation
15
+
16
+ # Ruby debugging extensions (currently GDB-specific, migrating to abstraction)
17
+ ├── object.py # Object inspection (rb-print)
18
+ ├── fiber.py # Fiber debugging (rb-fiber-scan-heap, rb-fiber-switch)
19
+ ├── heap.py # Heap scanning (rb-heap-scan)
20
+ ├── stack.py # Stack inspection
21
+
22
+ # Support modules
23
+ ├── value.py # Ruby VALUE interpretation
24
+ ├── constants.py # Ruby constant lookups
25
+ ├── command.py # Command argument parsing
26
+ ├── format.py # Output formatting
27
+
28
+ # Ruby type wrappers
29
+ ├── rbasic.py # RBasic
30
+ ├── rstring.py # RString
31
+ ├── rarray.py # RArray
32
+ ├── rhash.py # RHash
33
+ ├── rstruct.py # RStruct
34
+ ├── rsymbol.py # RSymbol
35
+ ├── rfloat.py # RFloat
36
+ ├── rbignum.py # RBignum
37
+ ├── rclass.py # RClass
38
+ └── rexception.py # RException
39
+ ```
40
+
41
+ ## How It Works
42
+
43
+ ### 1. Single Entry Point
44
+
45
+ Both GDB and LLDB load the same `init.py` file:
46
+
47
+ **GDB** (~/.gdbinit):
48
+ ```gdb
49
+ source /path/to/data/toolbox/init.py
50
+ ```
51
+
52
+ **LLDB** (~/.lldbinit):
53
+ ```lldb
54
+ command script import /path/to/data/toolbox/init.py
55
+ ```
56
+
57
+ ### 2. Auto-Detection
58
+
59
+ `init.py` imports `debugger.py`, which automatically detects which debugger is running:
60
+
61
+ ```python
62
+ import debugger # Auto-detects!
63
+
64
+ if debugger.DEBUGGER_NAME == 'gdb':
65
+ print("Running in GDB")
66
+ elif debugger.DEBUGGER_NAME == 'lldb':
67
+ print("Running in LLDB")
68
+ ```
69
+
70
+ ### 3. Unified Abstraction
71
+
72
+ The `debugger` module provides a common API:
73
+
74
+ ```python
75
+ import debugger
76
+
77
+ # These work in both GDB and LLDB:
78
+ value = debugger.parse_and_eval("$var")
79
+ type = debugger.lookup_type("struct RBasic")
80
+ value_cast = value.cast(type.pointer())
81
+ ```
82
+
83
+ ### 4. Extension Loading
84
+
85
+ `init.py` loads all Ruby debugging extensions, which register commands with the debugger.
86
+
87
+ ## Installation
88
+
89
+ ### GDB
90
+ ```bash
91
+ gem install toolbox
92
+ bake -g toolbox toolbox:gdb:install
93
+ ```
94
+
95
+ This adds to `~/.gdbinit`:
96
+ ```gdb
97
+ # Ruby Toolbox GDB Extensions
98
+ source /path/to/gem/data/toolbox/init.py
99
+ ```
100
+
101
+ ### LLDB
102
+ ```bash
103
+ gem install toolbox
104
+ bake -g toolbox toolbox:lldb:install
105
+ ```
106
+
107
+ This adds to `~/.lldbinit`:
108
+ ```lldb
109
+ # Ruby Toolbox LLDB Extensions
110
+ command script import /path/to/gem/data/toolbox/init.py
111
+ ```
112
+
113
+ ## Available Commands
114
+
115
+ ### Object Inspection
116
+ - `rb-print <expression> [--depth N]` - Print Ruby objects with recursion
117
+
118
+ ### Fiber Debugging
119
+ - `rb-fiber-scan-heap [--limit N]` - Scan heap for fiber objects
120
+ - `rb-fiber-scan-switch <index>` - Switch to a fiber from scan cache
121
+ - `rb-fiber-switch <fiber_value>` - Switch to a specific fiber
122
+ - `rb-fiber-scan-stack-trace-all` - Print stack traces for all cached fibers
123
+
124
+ ### Heap Scanning
125
+ - `rb-heap-scan [--type TYPE] [--limit N]` - Scan Ruby heap for objects
126
+
127
+ ### Stack Inspection
128
+ - `rb-stack-trace` - Print combined Ruby/C stack trace
129
+
130
+ ## Migration Status
131
+
132
+ ### ✅ Fully Abstracted
133
+ - Command argument parsing
134
+ - Output formatting
135
+ - Constant lookups
136
+
137
+ ### 🚧 Partially Abstracted
138
+ - Object inspection (uses `gdb` directly, needs migration to `debugger`)
139
+ - Fiber debugging (uses `gdb` directly, needs migration to `debugger`)
140
+ - Heap scanning (uses `gdb` directly, needs migration to `debugger`)
141
+
142
+ ### 📋 Migration Plan
143
+ 1. Update extension modules to import `debugger` instead of `gdb`
144
+ 2. Replace `gdb.parse_and_eval()` with `debugger.parse_and_eval()`
145
+ 3. Replace `gdb.lookup_type()` with `debugger.lookup_type()`
146
+ 4. Test with both GDB and LLDB
147
+ 5. Add LLDB-specific handling where needed
148
+
149
+ ## Testing
150
+
151
+ Test with GDB:
152
+ ```bash
153
+ gdb -q ruby
154
+ (gdb) help rb-print
155
+ (gdb) help rb-fiber-scan-heap
156
+ ```
157
+
158
+ Test with LLDB (once migrated):
159
+ ```bash
160
+ lldb ruby
161
+ (lldb) help rb-print
162
+ (lldb) help rb-fiber-scan-heap
163
+ ```
164
+
165
+ ## Documentation
166
+
167
+ - [debugger/README.md](debugger/README.md) - Abstraction layer documentation
168
+ - [debugger/ARCHITECTURE.md](debugger/ARCHITECTURE.md) - Architecture details
169
+ - [debugger/API_COMPARISON.md](debugger/API_COMPARISON.md) - GDB vs LLDB API reference
170
+ - [debugger/MIGRATION.md](debugger/MIGRATION.md) - Migration guide for code
171
+
172
+ ## Python Module Notes
173
+
174
+ ### Why `init.py` not `__init__.py`?
175
+
176
+ - `__init__.py` makes a directory a Python *package*
177
+ - `init.py` is a regular Python file loaded explicitly by debuggers
178
+ - GDB uses `source init.py`, LLDB uses `command script import init.py`
179
+ - Neither requires package structure, so we use a simple `init.py`
180
+
181
+ ### Import Path Setup
182
+
183
+ `init.py` adds its directory to `sys.path` so extensions can import each other:
184
+
185
+ ```python
186
+ import os, sys
187
+ toolbox_dir = os.path.dirname(os.path.abspath(__file__))
188
+ if toolbox_dir not in sys.path:
189
+ sys.path.insert(0, toolbox_dir)
190
+ ```
191
+
192
+ This allows:
193
+ ```python
194
+ import debugger # data/toolbox/debugger.py
195
+ import print # data/toolbox/print.py
196
+ import fiber # data/toolbox/fiber.py
197
+ from debugger import gdb # data/toolbox/debugger/gdb.py
198
+ ```
199
+
200
+ ## Contributing
201
+
202
+ When adding new extensions:
203
+
204
+ 1. Use the `debugger` abstraction for portability
205
+ 2. Test with both GDB and LLDB
206
+ 3. Add documentation and examples
207
+ 4. Update this README
208
+
209
+ ## See Also
210
+
211
+ - [GDB Python API](https://sourceware.org/gdb/current/onlinedocs/gdb/Python-API.html)
212
+ - [LLDB Python API](https://lldb.llvm.org/use/python-reference.html)
213
+ - [Ruby VM Internals](https://docs.ruby-lang.org/en/master/extension_rdoc.html)
214
+
@@ -0,0 +1,150 @@
1
+ import debugger
2
+ import constants
3
+ import format
4
+ import rvalue
5
+ import rstring
6
+ import rclass
7
+
8
+ class RException:
9
+ """Wrapper for Ruby exception objects."""
10
+
11
+ def __init__(self, exception_value):
12
+ """Initialize with a Ruby exception VALUE.
13
+
14
+ Args:
15
+ exception_value: A GDB value representing a Ruby exception object
16
+ """
17
+ self.value = exception_value
18
+ self._basic = None
19
+ self._klass = None
20
+ self._class_name = None
21
+ self._message = None
22
+
23
+ # Validate it's an object
24
+ if rvalue.is_immediate(exception_value):
25
+ raise ValueError("Exception VALUE cannot be an immediate value")
26
+
27
+ @property
28
+ def klass(self):
29
+ """Get the exception's class (klass pointer)."""
30
+ if self._klass is None:
31
+ if self._basic is None:
32
+ self._basic = self.value.cast(constants.type_struct('struct RBasic').pointer())
33
+ self._klass = self._basic['klass']
34
+ return self._klass
35
+
36
+ @property
37
+ def class_name(self):
38
+ """Get the exception class name as a string."""
39
+ if self._class_name is None:
40
+ self._class_name = self._get_class_name()
41
+ return self._class_name
42
+
43
+ @property
44
+ def message(self):
45
+ """Get the exception message as a string (None if unavailable)."""
46
+ if self._message is None:
47
+ self._message = self._get_message()
48
+ return self._message
49
+
50
+ def _get_class_name(self):
51
+ """Extract class name from klass pointer.
52
+
53
+ Uses the rclass module to get the class name.
54
+ """
55
+ try:
56
+ return rclass.get_class_name(self.klass)
57
+ except Exception:
58
+ # Fallback if we can't read the class
59
+ return f"Exception(klass=0x{int(self.klass):x})"
60
+
61
+ def _get_message(self):
62
+ """Extract message from exception object.
63
+
64
+ Walks instance variables to find 'mesg' ivar.
65
+ Returns None if message is unavailable (common in core dumps).
66
+ """
67
+ try:
68
+ # Exception objects store message in 'mesg' instance variable
69
+ # For now, return None as full ivar walking is complex
70
+ # TODO: Implement full instance variable walking for core dumps
71
+ return None
72
+ except Exception:
73
+ return None
74
+
75
+ def __str__(self):
76
+ """Return formatted string 'ClassName: message' or just 'ClassName'."""
77
+ class_name = self.class_name
78
+ msg = self.message
79
+
80
+ if msg:
81
+ return f"{class_name}: {msg}"
82
+ else:
83
+ return class_name
84
+
85
+ def print_to(self, terminal):
86
+ """Print this exception with formatting to the given terminal."""
87
+ class_name = self.class_name
88
+ msg = self.message
89
+
90
+ # Print class name with type formatting
91
+ class_output = terminal.print(
92
+ format.type, class_name,
93
+ format.reset
94
+ )
95
+
96
+ if msg:
97
+ # Print message with string formatting
98
+ msg_output = terminal.print(
99
+ format.string, f': {msg}',
100
+ format.reset
101
+ )
102
+ return f"{class_output}{msg_output}"
103
+ else:
104
+ return class_output
105
+
106
+ def print_recursive(self, printer, depth):
107
+ """Print this exception (no recursion needed for exceptions)."""
108
+ printer.print(self)
109
+
110
+ def is_exception(val):
111
+ """Check if a VALUE is an exception object.
112
+
113
+ Args:
114
+ val: A GDB value representing a Ruby VALUE
115
+
116
+ Returns:
117
+ True if the value appears to be an exception object, False otherwise
118
+ """
119
+ if not rvalue.is_object(val):
120
+ return False
121
+
122
+ try:
123
+ # Check if it's a T_OBJECT or T_DATA (exceptions can be either)
124
+ basic = val.cast(constants.type_struct("struct RBasic").pointer())
125
+ flags = int(basic['flags'])
126
+ type_flag = flags & constants.type("RUBY_T_MASK")
127
+
128
+ # Exceptions are typically T_OBJECT, but could also be T_DATA
129
+ t_object = constants.type("RUBY_T_OBJECT")
130
+ t_data = constants.type("RUBY_T_DATA")
131
+
132
+ return type_flag == t_object or type_flag == t_data
133
+ except Exception:
134
+ return False
135
+
136
+ def RExceptionFactory(val):
137
+ """Factory function to create RException or return None.
138
+
139
+ Args:
140
+ val: A GDB value representing a Ruby VALUE
141
+
142
+ Returns:
143
+ RException instance if val is an exception, None otherwise
144
+ """
145
+ if is_exception(val):
146
+ try:
147
+ return RException(val)
148
+ except Exception:
149
+ return None
150
+ return None
@@ -0,0 +1,88 @@
1
+ import debugger
2
+ import constants
3
+ import rbasic
4
+ import struct
5
+ import format
6
+
7
+ class RFloatImmediate:
8
+ """Flonum - immediate float encoding (Ruby 3.4+)"""
9
+ def __init__(self, value):
10
+ self.value = int(value)
11
+
12
+ def float_value(self):
13
+ # Flonum decoding from Ruby's rb_float_flonum_value
14
+ # Special case for 0.0
15
+ if self.value == 0x8000000000000002:
16
+ return 0.0
17
+
18
+ v = self.value
19
+ b63 = (v >> 63) & 1
20
+ # e: xx1... -> 011...
21
+ # xx0... -> 100...
22
+ # ^b63
23
+ adjusted = (2 - b63) | (v & ~0x03)
24
+ # RUBY_BIT_ROTR(x, 3) rotates right by 3 bits
25
+ # Which is: (x >> 3) | (x << (64 - 3))
26
+ rotated = ((adjusted >> 3) | (adjusted << 61)) & 0xFFFFFFFFFFFFFFFF
27
+
28
+ # Pack as unsigned long long, then unpack as double
29
+ return struct.unpack('d', struct.pack('Q', rotated))[0]
30
+
31
+ def __str__(self):
32
+ return f"<T_FLOAT> {self.float_value()}"
33
+
34
+ def print_to(self, terminal):
35
+ """Print formatted float representation."""
36
+ terminal.print_type_tag('T_FLOAT')
37
+ terminal.print(' ', end='')
38
+ terminal.print(format.number, str(self.float_value()), format.reset, end='')
39
+
40
+ def print_recursive(self, printer, depth):
41
+ """Print this float (no recursion needed)."""
42
+ printer.print(self)
43
+
44
+ class RFloatObject:
45
+ """Heap-allocated float object"""
46
+ def __init__(self, value):
47
+ self.value = value.cast(constants.type_struct('struct RFloat'))
48
+
49
+ def float_value(self):
50
+ return float(self.value['float_value'])
51
+
52
+ def __str__(self):
53
+ addr = int(self.value.address)
54
+ return f"<T_FLOAT@0x{addr:x}> {self.float_value()}"
55
+
56
+ def print_to(self, terminal):
57
+ """Print formatted float representation."""
58
+ addr = int(self.value.address)
59
+ terminal.print_type_tag('T_FLOAT', addr)
60
+ terminal.print(' ', end='')
61
+ terminal.print(format.number, str(self.float_value()), format.reset, end='')
62
+
63
+ def print_recursive(self, printer, depth):
64
+ """Print this float (no recursion needed)."""
65
+ printer.print(self)
66
+
67
+ def is_flonum(value):
68
+ """Check if value is an immediate flonum"""
69
+ import sys
70
+ val_int = int(value)
71
+ # FLONUM_MASK = 0x03, FLONUM_FLAG = 0x02 (on USE_FLONUM platforms)
72
+ # Get from ruby_special_consts enum
73
+ FLONUM_MASK = constants.get_enum('ruby_special_consts', 'RUBY_FLONUM_MASK', 0x03)
74
+ FLONUM_FLAG = constants.get_enum('ruby_special_consts', 'RUBY_FLONUM_FLAG', 0x02)
75
+ result = (val_int & FLONUM_MASK) == FLONUM_FLAG
76
+ return result
77
+
78
+ def RFloat(value):
79
+ """Factory function for float values - handles both flonums and heap objects"""
80
+ # Check for immediate flonum first
81
+ if is_flonum(value):
82
+ return RFloatImmediate(value)
83
+
84
+ # Check for heap-allocated T_FLOAT
85
+ if rbasic.is_type(value, 'RUBY_T_FLOAT'):
86
+ return RFloatObject(value)
87
+
88
+ return None
@@ -0,0 +1,151 @@
1
+ import debugger
2
+ import rbasic
3
+ import constants
4
+ import format
5
+
6
+ class RHashBase:
7
+ """Base class for RHash variants."""
8
+
9
+ def __init__(self, value):
10
+ """value is a VALUE pointing to a T_HASH object."""
11
+ self.value = value
12
+ self.rhash = value.cast(constants.type_struct('struct RHash').pointer())
13
+ self.basic = value.cast(constants.type_struct('struct RBasic').pointer())
14
+ self.flags = int(self.basic.dereference()['flags'])
15
+
16
+ def size(self):
17
+ """Get hash size. Must be implemented by subclasses."""
18
+ raise NotImplementedError
19
+
20
+ def pairs(self):
21
+ """Iterate over (key, value) pairs. Must be implemented by subclasses."""
22
+ raise NotImplementedError
23
+
24
+ class RHashSTTable(RHashBase):
25
+ """Hash using ST table structure (older or larger hashes)."""
26
+
27
+ def __init__(self, value):
28
+ super().__init__(value)
29
+ # Calculate st_table pointer
30
+ rhash_size = debugger.parse_and_eval("sizeof(struct RHash)")
31
+ st_table_addr = int(value) + int(rhash_size)
32
+ st_table_type = constants.type_struct("st_table")
33
+ self.st_table = debugger.create_value_from_address(st_table_addr, st_table_type).address
34
+
35
+ def size(self):
36
+ return int(self.st_table.dereference()['num_entries'])
37
+
38
+ def pairs(self):
39
+ """Yield (key, value) pairs."""
40
+ num_entries = self.size()
41
+ for i in range(num_entries):
42
+ key = self.st_table.dereference()['entries'][i]['key']
43
+ value = self.st_table.dereference()['entries'][i]['record']
44
+ yield (key, value)
45
+
46
+ def __str__(self):
47
+ """Return string representation of hash."""
48
+ addr = int(self.value)
49
+ return f"<T_HASH@0x{addr:x} ST-Table entries={self.size()}>"
50
+
51
+ def print_to(self, terminal):
52
+ """Print this hash with formatting."""
53
+ addr = int(self.value)
54
+ details = f"ST-Table entries={self.size()}"
55
+ terminal.print_type_tag('T_HASH', addr, details)
56
+
57
+ def print_recursive(self, printer, depth):
58
+ """Print this hash recursively."""
59
+ printer.print(self)
60
+
61
+ if depth <= 0:
62
+ if self.size() > 0:
63
+ printer.print_with_indent(printer.max_depth - depth, " ...")
64
+ return
65
+
66
+ # Print each key-value pair
67
+ for i, (key, value) in enumerate(self.pairs()):
68
+ printer.print_key_label(printer.max_depth - depth, i)
69
+ printer.print_value(key, depth - 1)
70
+ printer.print_value_label(printer.max_depth - depth)
71
+ printer.print_value(value, depth - 1)
72
+
73
+ class RHashARTable(RHashBase):
74
+ """Hash using AR table structure (newer, smaller hashes)."""
75
+
76
+ def __init__(self, value):
77
+ super().__init__(value)
78
+ # Feature detection: check if as.ar field exists (Ruby 3.2)
79
+ # vs embedded after RHash (Ruby 3.3+)
80
+ as_union = self.rhash.dereference()['as']
81
+ if as_union is not None:
82
+ # Ruby 3.2 layout: ar_table is accessed via as.ar pointer
83
+ self.ar_table = as_union['ar']
84
+ else:
85
+ # Ruby 3.3+: ar_table is embedded directly after RHash structure
86
+ rhash_size = debugger.parse_and_eval("sizeof(struct RHash)")
87
+ ar_table_addr = int(self.rhash) + int(rhash_size)
88
+ ar_table_type = constants.type_struct("struct ar_table_struct")
89
+ self.ar_table = debugger.create_value_from_address(ar_table_addr, ar_table_type).address
90
+
91
+ # Get array table size and bound from flags
92
+ self.ar_size = ((self.flags & constants.get("RHASH_AR_TABLE_SIZE_MASK")) >> constants.get("RHASH_AR_TABLE_SIZE_SHIFT"))
93
+ self.ar_bound = ((self.flags & constants.get("RHASH_AR_TABLE_BOUND_MASK")) >> constants.get("RHASH_AR_TABLE_BOUND_SHIFT"))
94
+
95
+ def size(self):
96
+ return self.ar_size
97
+
98
+ def bound(self):
99
+ """Get the bound (capacity) of the AR table."""
100
+ return self.ar_bound
101
+
102
+ def pairs(self):
103
+ """Yield (key, value) pairs, skipping undefined/deleted entries."""
104
+ RUBY_Qundef = constants.get("RUBY_Qundef")
105
+ for i in range(int(self.ar_bound)):
106
+ key = self.ar_table.dereference()['pairs'][i]['key']
107
+ # Skip undefined/deleted entries
108
+ if int(key) != RUBY_Qundef:
109
+ value = self.ar_table.dereference()['pairs'][i]['val']
110
+ yield (key, value)
111
+
112
+ def __str__(self):
113
+ """Return string representation of hash."""
114
+ addr = int(self.value)
115
+ return f"<T_HASH@0x{addr:x} AR-Table size={self.size()} bound={self.bound()}>"
116
+
117
+ def print_to(self, terminal):
118
+ """Print this hash with formatting."""
119
+ addr = int(self.value)
120
+ details = f"AR-Table size={self.size()} bound={self.bound()}"
121
+ terminal.print_type_tag('T_HASH', addr, details)
122
+
123
+ def print_recursive(self, printer, depth):
124
+ """Print this hash recursively."""
125
+ printer.print(self)
126
+
127
+ if depth <= 0:
128
+ if self.size() > 0:
129
+ printer.print_with_indent(printer.max_depth - depth, " ...")
130
+ return
131
+
132
+ # Print each key-value pair
133
+ for i, (key, value) in enumerate(self.pairs()):
134
+ printer.print_key_label(printer.max_depth - depth, i)
135
+ printer.print_value(key, depth - 1)
136
+ printer.print_value_label(printer.max_depth - depth)
137
+ printer.print_value(value, depth - 1)
138
+
139
+ def RHash(value):
140
+ """Factory function that returns the appropriate RHash variant.
141
+
142
+ Caller should ensure value is a RUBY_T_HASH before calling this function.
143
+ """
144
+ # Get flags to determine ST table vs AR table
145
+ basic = value.cast(constants.type_struct('struct RBasic').pointer())
146
+ flags = int(basic.dereference()['flags'])
147
+
148
+ if (flags & constants.get("RHASH_ST_TABLE_FLAG")) != 0:
149
+ return RHashSTTable(value)
150
+ else:
151
+ return RHashARTable(value)