toolbox 0.1.3 → 0.2.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 +178 -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 +254 -0
  13. data/data/toolbox/constants.py +200 -0
  14. data/data/toolbox/context.py +295 -0
  15. data/data/toolbox/debugger/__init__.py +99 -0
  16. data/data/toolbox/debugger/gdb_backend.py +595 -0
  17. data/data/toolbox/debugger/lldb_backend.py +885 -0
  18. data/data/toolbox/fiber.py +885 -0
  19. data/data/toolbox/format.py +200 -0
  20. data/data/toolbox/heap.py +669 -0
  21. data/data/toolbox/init.py +85 -0
  22. data/data/toolbox/object.py +84 -0
  23. data/data/toolbox/rarray.py +124 -0
  24. data/data/toolbox/rbasic.py +103 -0
  25. data/data/toolbox/rbignum.py +52 -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 +98 -0
  30. data/data/toolbox/rhash.py +159 -0
  31. data/data/toolbox/rstring.py +234 -0
  32. data/data/toolbox/rstruct.py +157 -0
  33. data/data/toolbox/rsymbol.py +302 -0
  34. data/data/toolbox/stack.py +630 -0
  35. data/data/toolbox/value.py +183 -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 +2 -0
  44. metadata +95 -165
  45. metadata.gz.sig +0 -0
  46. data/Rakefile +0 -57
  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,234 @@
1
+ import debugger
2
+ import rbasic
3
+ import constants
4
+ import format
5
+
6
+ # Type code for arrays (same in GDB and LLDB)
7
+ _TYPE_CODE_ARRAY = 1
8
+
9
+ def _char_ptr_type():
10
+ """Return char* type for GDB."""
11
+ return constants.type_struct('char').pointer()
12
+
13
+ class RStringBase:
14
+ """Base class for RString variants with common functionality."""
15
+
16
+ def __init__(self, value):
17
+ """value is a VALUE pointing to a T_STRING object."""
18
+ self.value = value
19
+ self.rstring = value.cast(constants.type_struct('struct RString').pointer())
20
+ self.basic = value.cast(constants.type_struct('struct RBasic').pointer())
21
+ self.flags = int(self.basic.dereference()['flags'])
22
+
23
+ def _is_embedded(self):
24
+ """Check if string is embedded. Must be implemented by subclasses."""
25
+ raise NotImplementedError
26
+
27
+ def length(self):
28
+ """Get string length. Must be implemented by subclasses."""
29
+ raise NotImplementedError
30
+
31
+ def data_ptr(self):
32
+ """Get pointer to string data. Must be implemented by subclasses."""
33
+ raise NotImplementedError
34
+
35
+ def read_bytes(self, max_fallback_scan=256):
36
+ """Return (bytes, length). If declared length is 0, will fallback to scanning for NUL up to max_fallback_scan."""
37
+ pointer = self.data_ptr()
38
+ length = self.length()
39
+
40
+ # Convert pointer to integer address
41
+ pointer_addr = int(pointer)
42
+
43
+ if length and length > 0:
44
+ # Known length - read exact amount
45
+ try:
46
+ data = debugger.read_memory(pointer_addr, length)
47
+ return (data, length)
48
+ except debugger.MemoryError:
49
+ return (b"", 0)
50
+ else:
51
+ # Unknown length - scan for NUL terminator
52
+ try:
53
+ return debugger.read_cstring(pointer_addr, max_fallback_scan)
54
+ except debugger.MemoryError:
55
+ return (b"", 0)
56
+
57
+ def to_str(self, encoding='utf-8'):
58
+ """Convert to Python string."""
59
+ data, length = self.read_bytes()
60
+ return data.decode(encoding, errors='replace')
61
+
62
+ def __str__(self):
63
+ """Return the string with type tag and metadata."""
64
+ addr = int(self.value)
65
+ storage = "embedded" if self._is_embedded() else "heap"
66
+ content = self.to_str()
67
+ return f"<T_STRING@0x{addr:x} {storage} length={self.length()}> {repr(content)}"
68
+
69
+ def print_to(self, terminal):
70
+ """Print this string with formatting."""
71
+ addr = int(self.value)
72
+ storage = "embedded" if self._is_embedded() else "heap"
73
+ content = self.to_str()
74
+ tag = terminal.print(
75
+ format.metadata, '<',
76
+ format.type, 'T_STRING',
77
+ format.metadata, f'@0x{addr:x} {storage} length={self.length()}>',
78
+ format.reset
79
+ )
80
+ # Use repr() to properly escape quotes, newlines, etc.
81
+ string_val = terminal.print(format.string, repr(content), format.reset)
82
+ return f"{tag} {string_val}"
83
+
84
+ def print_recursive(self, printer, depth):
85
+ """Print this string (no recursion needed for strings)."""
86
+ printer.print(self)
87
+
88
+ class RString34(RStringBase):
89
+ """Ruby 3.4+ strings: top-level len, as.embed.ary, FL_USER1 set = heap."""
90
+
91
+ def _is_embedded(self):
92
+ FL_USER1 = constants.flag('RUBY_FL_USER1')
93
+ # FL_USER1 set => heap (not embedded)
94
+ return (self.flags & FL_USER1) == 0
95
+
96
+ def length(self):
97
+ return int(self.rstring.dereference()['len'])
98
+
99
+ def data_ptr(self):
100
+ if not self._is_embedded():
101
+ return self.rstring.dereference()['as']['heap']['ptr']
102
+ # Embedded: use as.embed.ary
103
+ ary = self.rstring.dereference()['as']['embed']['ary']
104
+ try:
105
+ if ary.type.code == _TYPE_CODE_ARRAY:
106
+ return ary.address.cast(_char_ptr_type())
107
+ return ary
108
+ except Exception:
109
+ return ary.address.cast(_char_ptr_type())
110
+
111
+ class RString33(RStringBase):
112
+ """Ruby 3.3 strings: top-level len, as.embed.len exists, FL_USER1 set = embedded."""
113
+
114
+ def _is_embedded(self):
115
+ FL_USER1 = constants.flag('RUBY_FL_USER1')
116
+ # FL_USER1 set => embedded
117
+ return (self.flags & FL_USER1) != 0
118
+
119
+ def length(self):
120
+ return int(self.rstring.dereference()['len'])
121
+
122
+ def data_ptr(self):
123
+ if not self._is_embedded():
124
+ # FL_USER1 not set: in Ruby 3.3, the data may be in as.heap.ptr field
125
+ # For small strings, this is inline data (union overlaps with as.embed)
126
+ # We need the ADDRESS of this field to read the bytes, not the value
127
+ heap_ptr_field = self.rstring.dereference()['as']['heap']['ptr']
128
+ return heap_ptr_field.address.cast(_char_ptr_type())
129
+ # Embedded: use as.embed.ary
130
+ ary = self.rstring.dereference()['as']['embed']['ary']
131
+ try:
132
+ if ary.type.code == _TYPE_CODE_ARRAY:
133
+ return ary.address.cast(_char_ptr_type())
134
+ return ary
135
+ except Exception:
136
+ return ary.address.cast(_char_ptr_type())
137
+
138
+ class RString32RVARGC(RStringBase):
139
+ """Ruby 3.2 with RVARGC: as.embed.len for embedded, as.embed.ary for data."""
140
+
141
+ def _is_embedded(self):
142
+ FL_USER1 = constants.flag('RUBY_FL_USER1')
143
+ # FL_USER1 set => heap (not embedded)
144
+ return (self.flags & FL_USER1) == 0
145
+
146
+ def length(self):
147
+ if self._is_embedded():
148
+ return int(self.rstring.dereference()['as']['embed']['len'])
149
+ else:
150
+ return int(self.rstring.dereference()['as']['heap']['len'])
151
+
152
+ def data_ptr(self):
153
+ if not self._is_embedded():
154
+ return self.rstring.dereference()['as']['heap']['ptr']
155
+ # Embedded: use as.embed.ary
156
+ ary = self.rstring.dereference()['as']['embed']['ary']
157
+ try:
158
+ if ary.type.code == _TYPE_CODE_ARRAY:
159
+ return ary.address.cast(_char_ptr_type())
160
+ return ary
161
+ except Exception:
162
+ return ary.address.cast(_char_ptr_type())
163
+
164
+ class RString32Legacy(RStringBase):
165
+ """Legacy Ruby 3.2: embedded length in FL_USER2..6, data in as.ary."""
166
+
167
+ def _is_embedded(self):
168
+ FL_USER1 = constants.flag('RUBY_FL_USER1')
169
+ # FL_USER1 set => heap (not embedded)
170
+ return (self.flags & FL_USER1) == 0
171
+
172
+ def length(self):
173
+ if self._is_embedded():
174
+ FL2 = constants.flag('RUBY_FL_USER2')
175
+ FL3 = constants.flag('RUBY_FL_USER3')
176
+ FL4 = constants.flag('RUBY_FL_USER4')
177
+ FL5 = constants.flag('RUBY_FL_USER5')
178
+ FL6 = constants.flag('RUBY_FL_USER6')
179
+ USHIFT = constants.flag('RUBY_FL_USHIFT')
180
+ mask = FL2 | FL3 | FL4 | FL5 | FL6
181
+ return (self.flags & mask) >> (USHIFT + 2)
182
+ else:
183
+ return int(self.rstring.dereference()['as']['heap']['len'])
184
+
185
+ def data_ptr(self):
186
+ if not self._is_embedded():
187
+ return self.rstring.dereference()['as']['heap']['ptr']
188
+ # Embedded: use as.ary (not as.embed.ary)
189
+ ary = self.rstring.dereference()['as']['ary']
190
+ try:
191
+ if ary.type.code == _TYPE_CODE_ARRAY:
192
+ return ary.address.cast(_char_ptr_type())
193
+ return ary
194
+ except Exception:
195
+ return ary.address.cast(_char_ptr_type())
196
+
197
+ def RString(value):
198
+ """Factory function that detects the RString variant and returns the appropriate instance.
199
+
200
+ Caller should ensure value is a RUBY_T_STRING before calling this function.
201
+
202
+ Detects at runtime whether the process uses:
203
+ - Ruby 3.4+: top-level len field, as.embed.ary (no embed.len), FL_USER1 set = heap
204
+ - Ruby 3.3: top-level len field, as.embed.len exists, FL_USER1 set = embedded
205
+ - Ruby 3.2 with RVARGC: as.embed.len for embedded, as.embed.ary for data
206
+ - Legacy 3.2: embedded length in FL_USER2..6, data in as.ary
207
+ """
208
+ rstring = value.cast(constants.type_struct('struct RString').pointer())
209
+
210
+ # Try top-level len field (Ruby 3.3+/3.4+)
211
+ len_field = rstring.dereference()['len']
212
+ if len_field is not None:
213
+ # Now check if embed structure has len field (3.3) or just ary (3.4+)
214
+ as_union = rstring.dereference()['as']
215
+ if as_union is not None:
216
+ embed_struct = as_union['embed']
217
+ if embed_struct is not None:
218
+ embed_len = embed_struct['len']
219
+ if embed_len is not None:
220
+ return RString33(value)
221
+ else:
222
+ return RString34(value)
223
+
224
+ # Try RVARGC embedded len field (3.2 RVARGC)
225
+ as_union = rstring.dereference()['as']
226
+ if as_union is not None:
227
+ embed_struct = as_union['embed']
228
+ if embed_struct is not None:
229
+ embed_len = embed_struct['len']
230
+ if embed_len is not None:
231
+ return RString32RVARGC(value)
232
+
233
+ # Fallback to legacy 3.2
234
+ return RString32Legacy(value)
@@ -0,0 +1,157 @@
1
+ import debugger
2
+ import rbasic
3
+ import constants
4
+ import format
5
+
6
+
7
+ class RStructBase:
8
+ """Base class for Ruby struct variants."""
9
+
10
+ def __init__(self, value):
11
+ """value is a VALUE pointing to a T_STRUCT object."""
12
+ self.value = value
13
+ self.rstruct = value.cast(constants.type_struct('struct RStruct').pointer())
14
+
15
+ def length(self):
16
+ """Get the number of fields in the struct."""
17
+ raise NotImplementedError("Subclass must implement length()")
18
+
19
+ def items_ptr(self):
20
+ """Get pointer to the struct's data array."""
21
+ raise NotImplementedError("Subclass must implement items_ptr()")
22
+
23
+ def __len__(self):
24
+ """Support len(struct) syntax."""
25
+ return self.length()
26
+
27
+ def __getitem__(self, index):
28
+ """Support struct[i] syntax to access fields."""
29
+ length = self.length()
30
+ if index < 0 or index >= length:
31
+ raise IndexError(f"struct index out of range: {index} (length: {length})")
32
+
33
+ ptr = self.items_ptr()
34
+ return ptr[index]
35
+
36
+
37
+ class RStructEmbedded(RStructBase):
38
+ """Ruby struct with embedded storage (small structs)."""
39
+
40
+ def length(self):
41
+ """Get length from flags for embedded struct."""
42
+ flags = int(self.rstruct.dereference()['basic']['flags'])
43
+
44
+ # Extract length from FL_USER1 and FL_USER2 flags
45
+ RUBY_FL_USER1 = constants.flag("RUBY_FL_USER1")
46
+ RUBY_FL_USER2 = constants.flag("RUBY_FL_USER2")
47
+ RUBY_FL_USHIFT = constants.flag("RUBY_FL_USHIFT")
48
+
49
+ mask = RUBY_FL_USER1 | RUBY_FL_USER2
50
+ shift = RUBY_FL_USHIFT + 1
51
+ return (flags & mask) >> shift
52
+
53
+ def items_ptr(self):
54
+ """Get pointer to embedded data array."""
55
+ return self.rstruct.dereference()['as']['ary']
56
+
57
+ def __str__(self):
58
+ """Return string representation of struct."""
59
+ addr = int(self.value)
60
+ return f"<T_STRUCT@0x{addr:x} embedded length={len(self)}>"
61
+
62
+ def print_to(self, terminal):
63
+ """Return formatted struct representation."""
64
+ addr = int(self.value)
65
+ return terminal.print(
66
+ format.metadata, '<',
67
+ format.type, 'T_STRUCT',
68
+ format.metadata, f'@0x{addr:x} embedded length={len(self)}>',
69
+ format.reset
70
+ )
71
+
72
+ def print_recursive(self, printer, depth):
73
+ """Print this struct recursively."""
74
+ printer.print(self)
75
+
76
+ if depth <= 0:
77
+ if len(self) > 0:
78
+ printer.print_with_indent(printer.max_depth - depth, " ...")
79
+ return
80
+
81
+ # Print each field
82
+ for i in range(len(self)):
83
+ printer.print_item_label(printer.max_depth - depth, i)
84
+ printer.print_value(self[i], depth - 1)
85
+
86
+
87
+ class RStructHeap(RStructBase):
88
+ """Ruby struct with heap-allocated storage (large structs)."""
89
+
90
+ def length(self):
91
+ """Get length from heap structure."""
92
+ return int(self.rstruct.dereference()['as']['heap']['len'])
93
+
94
+ def items_ptr(self):
95
+ """Get pointer to heap-allocated data array."""
96
+ return self.rstruct.dereference()['as']['heap']['ptr']
97
+
98
+ def __str__(self):
99
+ """Return string representation of struct."""
100
+ addr = int(self.value)
101
+ return f"<T_STRUCT@0x{addr:x} heap length={len(self)}>"
102
+
103
+ def print_to(self, terminal):
104
+ """Return formatted struct representation."""
105
+ addr = int(self.value)
106
+ return terminal.print(
107
+ format.metadata, '<',
108
+ format.type, 'T_STRUCT',
109
+ format.metadata, f'@0x{addr:x} heap length={len(self)}>',
110
+ format.reset
111
+ )
112
+
113
+ def print_recursive(self, printer, depth):
114
+ """Print this struct recursively."""
115
+ printer.print(self)
116
+
117
+ if depth <= 0:
118
+ if len(self) > 0:
119
+ printer.print_with_indent(printer.max_depth - depth, " ...")
120
+ return
121
+
122
+ # Print each field
123
+ for i in range(len(self)):
124
+ printer.print_item_label(printer.max_depth - depth, i)
125
+ printer.print_value(self[i], depth - 1)
126
+
127
+
128
+ def RStruct(value):
129
+ """
130
+ Factory function to create the appropriate RStruct variant.
131
+
132
+ Caller should ensure value is a RUBY_T_STRUCT before calling this function.
133
+
134
+ Returns:
135
+ RStructEmbedded or RStructHeap instance
136
+ """
137
+ # Cast to RStruct pointer to read flags
138
+ rstruct_type = constants.type_struct("struct RStruct").pointer()
139
+ rstruct = value.cast(rstruct_type)
140
+ flags = int(rstruct.dereference()['basic']['flags'])
141
+
142
+ # Feature detection: check for RSTRUCT_EMBED_LEN_MASK flag
143
+ # If struct uses embedded storage, the length is encoded in flags
144
+ RSTRUCT_EMBED_LEN_MASK = constants.get("RSTRUCT_EMBED_LEN_MASK")
145
+ if RSTRUCT_EMBED_LEN_MASK is None:
146
+ # Fallback: try to detect by checking if as.heap exists
147
+ try:
148
+ _ = rstruct.dereference()['as']['heap']
149
+ return RStructHeap(value)
150
+ except Exception:
151
+ return RStructEmbedded(value)
152
+
153
+ # Check if embedded flag is set
154
+ if flags & RSTRUCT_EMBED_LEN_MASK:
155
+ return RStructEmbedded(value)
156
+ else:
157
+ return RStructHeap(value)
@@ -0,0 +1,302 @@
1
+ import debugger
2
+ import rbasic
3
+ import constants
4
+ import format
5
+
6
+
7
+ class RubySymbol:
8
+ """Ruby symbol from ID (integer identifier)."""
9
+
10
+ def __init__(self, symbol_id):
11
+ """Initialize from a symbol ID.
12
+
13
+ Args:
14
+ symbol_id: Integer symbol ID
15
+ """
16
+ self.symbol_id = int(symbol_id)
17
+ self._name = None
18
+
19
+ def to_str(self):
20
+ """Get symbol name from global symbol table, or None if not found."""
21
+ import sys
22
+ if self._name is not None:
23
+ return self._name
24
+
25
+ try:
26
+ # rb_id_to_serial
27
+ # tLAST_OP_ID is in enum ruby_method_ids
28
+ tLAST_OP_ID = constants.get_enum('ruby_method_ids', 'tLAST_OP_ID', 163)
29
+ ID_ENTRY_UNIT = 512
30
+ ID_ENTRY_SIZE = 2
31
+
32
+ id_val = self.symbol_id
33
+
34
+ # The symbol_id is already the ID value (shifted from VALUE by RUBY_SPECIAL_SHIFT)
35
+ # We just need to convert it to a serial number for the global symbol table lookup
36
+ if id_val > tLAST_OP_ID:
37
+ # For dynamic symbols, the serial is stored in the upper bits
38
+ try:
39
+ RUBY_ID_SCOPE_SHIFT = int(constants.get_enum('ruby_id_types', 'RUBY_ID_SCOPE_SHIFT', 4))
40
+ except Exception:
41
+ RUBY_ID_SCOPE_SHIFT = 4 # Default value
42
+ serial = id_val >> RUBY_ID_SCOPE_SHIFT
43
+ else:
44
+ # For static symbols (operators, etc.), the ID is the serial
45
+ serial = id_val
46
+
47
+ # Access ruby_global_symbols
48
+ global_symbols = debugger.parse_and_eval("ruby_global_symbols")
49
+
50
+ # Ruby 3.5+ changed from last_id to next_id
51
+ last_id_field = global_symbols['last_id']
52
+ if last_id_field is not None:
53
+ last_id = int(last_id_field)
54
+ else:
55
+ # Ruby 3.5+ uses next_id instead
56
+ next_id_field = global_symbols['next_id']
57
+ if next_id_field is not None:
58
+ last_id = int(next_id_field) - 1
59
+ else:
60
+ return None
61
+
62
+ if not serial or serial > last_id:
63
+ return None
64
+
65
+ idx = serial // ID_ENTRY_UNIT
66
+
67
+ # Get ids array
68
+ ids = global_symbols['ids'].cast(constants.type_struct("struct RArray").pointer())
69
+
70
+ # Import rarray to access the array
71
+ import rarray
72
+ arr = rarray.RArray(global_symbols['ids'])
73
+ if arr is None:
74
+ return None
75
+
76
+ # Get the symbols entry
77
+ entry_val = arr[idx]
78
+
79
+ # Get the symbol from the entry
80
+ entry_arr = rarray.RArray(entry_val)
81
+ if entry_arr is None:
82
+ return None
83
+
84
+ ids_idx = (serial % ID_ENTRY_UNIT) * ID_ENTRY_SIZE
85
+ sym_val = entry_arr[ids_idx]
86
+
87
+ # Get the string from the symbol
88
+ import rstring
89
+ str_obj = rstring.RString(sym_val)
90
+ if str_obj is None:
91
+ return None
92
+
93
+ self._name = str_obj.to_str()
94
+ return self._name
95
+
96
+ except Exception as e:
97
+ import traceback
98
+ traceback.print_exc(file=sys.stderr)
99
+ return None
100
+
101
+ def __str__(self):
102
+ """Return symbol representation."""
103
+ name = self.to_str()
104
+ if name:
105
+ return f":{name}"
106
+ else:
107
+ return f":id_0x{self.symbol_id:x}"
108
+
109
+
110
+ class RSymbolImmediate:
111
+ """Ruby immediate symbol (encoded in VALUE)."""
112
+
113
+ def __init__(self, value):
114
+ self.value = value
115
+ self._ruby_symbol = None
116
+
117
+ def id(self):
118
+ """Get the symbol ID."""
119
+ return get_id(self.value)
120
+
121
+ def ruby_symbol(self):
122
+ """Get RubySymbol instance for this symbol."""
123
+ if self._ruby_symbol is None:
124
+ self._ruby_symbol = RubySymbol(self.id())
125
+ return self._ruby_symbol
126
+
127
+ def to_str(self):
128
+ """Get symbol name from global symbol table, or None if not found."""
129
+ return self.ruby_symbol().to_str()
130
+
131
+ def __str__(self):
132
+ """Return symbol representation."""
133
+ name = self.to_str()
134
+ if name:
135
+ return f"<T_SYMBOL> :{name}"
136
+ else:
137
+ return f"<T_SYMBOL> :id_0x{self.id():x}"
138
+
139
+ def print_to(self, terminal):
140
+ """Return formatted symbol representation."""
141
+ name = self.to_str()
142
+ if name:
143
+ tag = terminal.print(
144
+ format.metadata, '<',
145
+ format.type, 'T_SYMBOL',
146
+ format.metadata, '>',
147
+ format.reset
148
+ )
149
+ symbol_val = terminal.print(format.symbol, f':{name}', format.reset)
150
+ return f"{tag} {symbol_val}"
151
+ else:
152
+ tag = terminal.print(
153
+ format.metadata, '<',
154
+ format.type, 'T_SYMBOL',
155
+ format.metadata, '>',
156
+ format.reset
157
+ )
158
+ symbol_val = terminal.print(format.symbol, f':id_0x{self.id():x}', format.reset)
159
+ return f"{tag} {symbol_val}"
160
+
161
+ def print_recursive(self, printer, depth):
162
+ """Print this symbol (no recursion needed)."""
163
+ printer.print(self)
164
+
165
+
166
+ class RSymbolObject:
167
+ """Ruby T_SYMBOL heap object."""
168
+
169
+ def __init__(self, value):
170
+ self.value = value
171
+ self.rsymbol = value.cast(constants.type_struct("struct RSymbol").pointer())
172
+ self._ruby_symbol = None
173
+
174
+ def id(self):
175
+ """Get the symbol ID."""
176
+ return int(self.rsymbol.dereference()['id'])
177
+
178
+ def ruby_symbol(self):
179
+ """Get RubySymbol instance for this symbol."""
180
+ if self._ruby_symbol is None:
181
+ self._ruby_symbol = RubySymbol(self.id())
182
+ return self._ruby_symbol
183
+
184
+ def fstr(self):
185
+ """Get the frozen string containing the symbol name."""
186
+ return self.rsymbol.dereference()['fstr']
187
+
188
+ def to_str(self):
189
+ """Get symbol name as a string."""
190
+ try:
191
+ import rstring
192
+ fstr_val = self.fstr()
193
+ str_obj = rstring.RString(fstr_val)
194
+ if str_obj:
195
+ return str_obj.to_str()
196
+ return None
197
+ except Exception:
198
+ return None
199
+
200
+ def __str__(self):
201
+ """Return symbol representation."""
202
+ name = self.to_str()
203
+ addr = int(self.value)
204
+ if name:
205
+ return f"<T_SYMBOL@0x{addr:x}> :{name}"
206
+ else:
207
+ fstr_val = self.fstr()
208
+ return f"<T_SYMBOL@0x{addr:x}> :<Symbol:0x{int(fstr_val):x}>"
209
+
210
+ def print_to(self, terminal):
211
+ """Return formatted symbol representation."""
212
+ name = self.to_str()
213
+ addr = int(self.value)
214
+ if name:
215
+ tag = terminal.print(
216
+ format.metadata, '<',
217
+ format.type, 'T_SYMBOL',
218
+ format.metadata, f'@0x{addr:x}>',
219
+ format.reset
220
+ )
221
+ symbol_val = terminal.print(format.symbol, f':{name}', format.reset)
222
+ return f"{tag} {symbol_val}"
223
+ else:
224
+ fstr_val = self.fstr()
225
+ tag = terminal.print(
226
+ format.metadata, '<',
227
+ format.type, 'T_SYMBOL',
228
+ format.metadata, f'@0x{addr:x}>',
229
+ format.reset
230
+ )
231
+ symbol_val = terminal.print(format.symbol, f':<Symbol:0x{int(fstr_val):x}>', format.reset)
232
+ return f"{tag} {symbol_val}"
233
+
234
+ def print_recursive(self, printer, depth):
235
+ """Print this symbol (no recursion needed)."""
236
+ printer.print(self)
237
+
238
+
239
+
240
+ def is_symbol(value):
241
+ """
242
+ Check if a VALUE is a symbol using the logic from Ruby's .gdbinit.
243
+
244
+ From .gdbinit:
245
+ if (($arg0) & ~(~(VALUE)0<<RUBY_SPECIAL_SHIFT)) == RUBY_SYMBOL_FLAG
246
+
247
+ This checks if the low bits (after masking RUBY_SPECIAL_SHIFT) match RUBY_SYMBOL_FLAG.
248
+ """
249
+ import sys
250
+ try:
251
+ val_int = int(value)
252
+
253
+ # Get Ruby constants from ruby_special_consts enum
254
+ RUBY_SPECIAL_SHIFT = constants.get_enum('ruby_special_consts', 'RUBY_SPECIAL_SHIFT', 8)
255
+ RUBY_SYMBOL_FLAG = constants.get_enum('ruby_special_consts', 'RUBY_SYMBOL_FLAG', 0x0c)
256
+
257
+ # Create mask for low bits: ~(~0 << RUBY_SPECIAL_SHIFT)
258
+ # This gives us RUBY_SPECIAL_SHIFT low bits set to 1
259
+ mask = ~(~0 << RUBY_SPECIAL_SHIFT)
260
+
261
+ # Check if masked value equals RUBY_SYMBOL_FLAG
262
+ result = (val_int & mask) == RUBY_SYMBOL_FLAG
263
+ return result
264
+ except Exception as e:
265
+ # Fallback to simple check
266
+ return (int(value) & 0xFF) == 0x0C
267
+
268
+
269
+ def get_id(value):
270
+ """
271
+ Extract symbol ID from a VALUE using the logic from Ruby's .gdbinit.
272
+
273
+ From .gdbinit:
274
+ set $id = (($arg0) >> RUBY_SPECIAL_SHIFT)
275
+ """
276
+ import sys
277
+ try:
278
+ RUBY_SPECIAL_SHIFT = constants.get_enum('ruby_special_consts', 'RUBY_SPECIAL_SHIFT', 8)
279
+ val_int = int(value)
280
+ result = val_int >> RUBY_SPECIAL_SHIFT
281
+ return result
282
+ except Exception as e:
283
+ # Fallback to shift by 8
284
+ return int(value) >> 8
285
+
286
+
287
+ def RSymbol(value):
288
+ """
289
+ Factory function to create the appropriate RSymbol variant.
290
+
291
+ Returns:
292
+ RSymbolImmediate, RSymbolObject, or None
293
+ """
294
+ # Check if it's an immediate symbol
295
+ if is_symbol(value):
296
+ return RSymbolImmediate(value)
297
+
298
+ # Check if it's a T_SYMBOL object
299
+ if rbasic.is_type(value, 'RUBY_T_SYMBOL'):
300
+ return RSymbolObject(value)
301
+
302
+ return None