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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/bake/ruby/gdb.rb +135 -0
- data/bake/toolbox/gdb.rb +137 -0
- data/bake/toolbox/lldb.rb +137 -0
- data/context/fiber-debugging.md +171 -0
- data/context/getting-started.md +178 -0
- data/context/heap-debugging.md +351 -0
- data/context/index.yaml +28 -0
- data/context/object-inspection.md +208 -0
- data/context/stack-inspection.md +188 -0
- data/data/toolbox/command.py +254 -0
- data/data/toolbox/constants.py +200 -0
- data/data/toolbox/context.py +295 -0
- data/data/toolbox/debugger/__init__.py +99 -0
- data/data/toolbox/debugger/gdb_backend.py +595 -0
- data/data/toolbox/debugger/lldb_backend.py +885 -0
- data/data/toolbox/fiber.py +885 -0
- data/data/toolbox/format.py +200 -0
- data/data/toolbox/heap.py +669 -0
- data/data/toolbox/init.py +85 -0
- data/data/toolbox/object.py +84 -0
- data/data/toolbox/rarray.py +124 -0
- data/data/toolbox/rbasic.py +103 -0
- data/data/toolbox/rbignum.py +52 -0
- data/data/toolbox/rclass.py +136 -0
- data/data/toolbox/readme.md +214 -0
- data/data/toolbox/rexception.py +150 -0
- data/data/toolbox/rfloat.py +98 -0
- data/data/toolbox/rhash.py +159 -0
- data/data/toolbox/rstring.py +234 -0
- data/data/toolbox/rstruct.py +157 -0
- data/data/toolbox/rsymbol.py +302 -0
- data/data/toolbox/stack.py +630 -0
- data/data/toolbox/value.py +183 -0
- data/lib/toolbox/gdb.rb +21 -0
- data/lib/toolbox/lldb.rb +21 -0
- data/lib/toolbox/version.rb +7 -1
- data/lib/toolbox.rb +9 -24
- data/license.md +21 -0
- data/readme.md +64 -0
- data/releases.md +9 -0
- data.tar.gz.sig +2 -0
- metadata +95 -165
- metadata.gz.sig +0 -0
- data/Rakefile +0 -57
- data/lib/dirs.rb +0 -9
- data/lib/toolbox/config.rb +0 -211
- data/lib/toolbox/default_controller.rb +0 -393
- data/lib/toolbox/helpers.rb +0 -11
- data/lib/toolbox/rendering.rb +0 -413
- data/lib/toolbox/searching.rb +0 -85
- data/lib/toolbox/session_params.rb +0 -63
- data/lib/toolbox/sorting.rb +0 -74
- data/locale/de/LC_MESSAGES/toolbox.mo +0 -0
- data/public/images/add.png +0 -0
- data/public/images/arrow_down.gif +0 -0
- data/public/images/arrow_up.gif +0 -0
- data/public/images/close.png +0 -0
- data/public/images/edit.gif +0 -0
- data/public/images/email.png +0 -0
- data/public/images/page.png +0 -0
- data/public/images/page_acrobat.png +0 -0
- data/public/images/page_add.png +0 -0
- data/public/images/page_copy.png +0 -0
- data/public/images/page_delete.png +0 -0
- data/public/images/page_edit.png +0 -0
- data/public/images/page_excel.png +0 -0
- data/public/images/page_list.png +0 -0
- data/public/images/page_save.png +0 -0
- data/public/images/page_word.png +0 -0
- data/public/images/remove.png +0 -0
- data/public/images/show.gif +0 -0
- data/public/images/spinner.gif +0 -0
- data/public/javascripts/popup.js +0 -498
- data/public/javascripts/toolbox.js +0 -18
- data/public/stylesheets/context_menu.css +0 -168
- data/public/stylesheets/popup.css +0 -30
- data/public/stylesheets/toolbox.css +0 -107
- data/view/toolbox/_collection.html.erb +0 -24
- data/view/toolbox/_collection_header.html.erb +0 -7
- data/view/toolbox/_context_menu.html.erb +0 -17
- data/view/toolbox/_dialogs.html.erb +0 -6
- data/view/toolbox/_form.html.erb +0 -30
- data/view/toolbox/_form_collection_row.html.erb +0 -18
- data/view/toolbox/_form_fieldset.html.erb +0 -30
- data/view/toolbox/_form_fieldset_row.html.erb +0 -19
- data/view/toolbox/_list.html.erb +0 -25
- data/view/toolbox/_list_row.html.erb +0 -10
- data/view/toolbox/_menu.html.erb +0 -7
- data/view/toolbox/_search_field.html.erb +0 -8
- data/view/toolbox/_show.html.erb +0 -12
- data/view/toolbox/_show_collection_row.html.erb +0 -6
- data/view/toolbox/_show_fieldset.html.erb +0 -21
- data/view/toolbox/edit.html.erb +0 -5
- data/view/toolbox/index.html.erb +0 -3
- data/view/toolbox/new.html.erb +0 -9
- 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-object-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 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 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-object-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-print` - Print Ruby stack (coming soon)
|
|
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-object-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-object-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 object # data/toolbox/object.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 value
|
|
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 value.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 value.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,98 @@
|
|
|
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
|
+
"""Return formatted float representation."""
|
|
36
|
+
tag = terminal.print(
|
|
37
|
+
format.metadata, '<',
|
|
38
|
+
format.type, 'T_FLOAT',
|
|
39
|
+
format.metadata, '>',
|
|
40
|
+
format.reset
|
|
41
|
+
)
|
|
42
|
+
num_val = terminal.print(format.number, str(self.float_value()), format.reset)
|
|
43
|
+
return f"{tag} {num_val}"
|
|
44
|
+
|
|
45
|
+
def print_recursive(self, printer, depth):
|
|
46
|
+
"""Print this float (no recursion needed)."""
|
|
47
|
+
printer.print(self)
|
|
48
|
+
|
|
49
|
+
class RFloatObject:
|
|
50
|
+
"""Heap-allocated float object"""
|
|
51
|
+
def __init__(self, value):
|
|
52
|
+
self.value = value.cast(constants.type_struct('struct RFloat'))
|
|
53
|
+
|
|
54
|
+
def float_value(self):
|
|
55
|
+
return float(self.value['float_value'])
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
addr = int(self.value.address)
|
|
59
|
+
return f"<T_FLOAT@0x{addr:x}> {self.float_value()}"
|
|
60
|
+
|
|
61
|
+
def print_to(self, terminal):
|
|
62
|
+
"""Return formatted float representation."""
|
|
63
|
+
addr = int(self.value.address)
|
|
64
|
+
tag = terminal.print(
|
|
65
|
+
format.metadata, '<',
|
|
66
|
+
format.type, 'T_FLOAT',
|
|
67
|
+
format.metadata, f'@0x{addr:x}>',
|
|
68
|
+
format.reset
|
|
69
|
+
)
|
|
70
|
+
num_val = terminal.print(format.number, str(self.float_value()), format.reset)
|
|
71
|
+
return f"{tag} {num_val}"
|
|
72
|
+
|
|
73
|
+
def print_recursive(self, printer, depth):
|
|
74
|
+
"""Print this float (no recursion needed)."""
|
|
75
|
+
printer.print(self)
|
|
76
|
+
|
|
77
|
+
def is_flonum(value):
|
|
78
|
+
"""Check if value is an immediate flonum"""
|
|
79
|
+
import sys
|
|
80
|
+
val_int = int(value)
|
|
81
|
+
# FLONUM_MASK = 0x03, FLONUM_FLAG = 0x02 (on USE_FLONUM platforms)
|
|
82
|
+
# Get from ruby_special_consts enum
|
|
83
|
+
FLONUM_MASK = constants.get_enum('ruby_special_consts', 'RUBY_FLONUM_MASK', 0x03)
|
|
84
|
+
FLONUM_FLAG = constants.get_enum('ruby_special_consts', 'RUBY_FLONUM_FLAG', 0x02)
|
|
85
|
+
result = (val_int & FLONUM_MASK) == FLONUM_FLAG
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
def RFloat(value):
|
|
89
|
+
"""Factory function for float values - handles both flonums and heap objects"""
|
|
90
|
+
# Check for immediate flonum first
|
|
91
|
+
if is_flonum(value):
|
|
92
|
+
return RFloatImmediate(value)
|
|
93
|
+
|
|
94
|
+
# Check for heap-allocated T_FLOAT
|
|
95
|
+
if rbasic.is_type(value, 'RUBY_T_FLOAT'):
|
|
96
|
+
return RFloatObject(value)
|
|
97
|
+
|
|
98
|
+
return None
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
return terminal.print(
|
|
55
|
+
format.metadata, '<',
|
|
56
|
+
format.type, 'T_HASH',
|
|
57
|
+
format.metadata, f'@0x{addr:x} ST-Table entries={self.size()}>',
|
|
58
|
+
format.reset
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def print_recursive(self, printer, depth):
|
|
62
|
+
"""Print this hash recursively."""
|
|
63
|
+
printer.print(self)
|
|
64
|
+
|
|
65
|
+
if depth <= 0:
|
|
66
|
+
if self.size() > 0:
|
|
67
|
+
printer.print_with_indent(printer.max_depth - depth, " ...")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# Print each key-value pair
|
|
71
|
+
for i, (key, value) in enumerate(self.pairs()):
|
|
72
|
+
printer.print_key_label(printer.max_depth - depth, i)
|
|
73
|
+
printer.print_value(key, depth - 1)
|
|
74
|
+
printer.print_value_label(printer.max_depth - depth)
|
|
75
|
+
printer.print_value(value, depth - 1)
|
|
76
|
+
|
|
77
|
+
class RHashARTable(RHashBase):
|
|
78
|
+
"""Hash using AR table structure (newer, smaller hashes)."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, value):
|
|
81
|
+
super().__init__(value)
|
|
82
|
+
# Feature detection: check if as.ar field exists (Ruby 3.2)
|
|
83
|
+
# vs embedded after RHash (Ruby 3.3+)
|
|
84
|
+
as_union = self.rhash.dereference()['as']
|
|
85
|
+
if as_union is not None:
|
|
86
|
+
# Ruby 3.2 layout: ar_table is accessed via as.ar pointer
|
|
87
|
+
self.ar_table = as_union['ar']
|
|
88
|
+
else:
|
|
89
|
+
# Ruby 3.3+: ar_table is embedded directly after RHash structure
|
|
90
|
+
rhash_size = debugger.parse_and_eval("sizeof(struct RHash)")
|
|
91
|
+
ar_table_addr = int(self.rhash) + int(rhash_size)
|
|
92
|
+
ar_table_type = constants.type_struct("struct ar_table_struct")
|
|
93
|
+
self.ar_table = debugger.create_value_from_address(ar_table_addr, ar_table_type).address
|
|
94
|
+
|
|
95
|
+
# Get array table size and bound from flags
|
|
96
|
+
self.ar_size = ((self.flags & constants.get("RHASH_AR_TABLE_SIZE_MASK")) >> constants.get("RHASH_AR_TABLE_SIZE_SHIFT"))
|
|
97
|
+
self.ar_bound = ((self.flags & constants.get("RHASH_AR_TABLE_BOUND_MASK")) >> constants.get("RHASH_AR_TABLE_BOUND_SHIFT"))
|
|
98
|
+
|
|
99
|
+
def size(self):
|
|
100
|
+
return self.ar_size
|
|
101
|
+
|
|
102
|
+
def bound(self):
|
|
103
|
+
"""Get the bound (capacity) of the AR table."""
|
|
104
|
+
return self.ar_bound
|
|
105
|
+
|
|
106
|
+
def pairs(self):
|
|
107
|
+
"""Yield (key, value) pairs, skipping undefined/deleted entries."""
|
|
108
|
+
RUBY_Qundef = constants.get("RUBY_Qundef")
|
|
109
|
+
for i in range(int(self.ar_bound)):
|
|
110
|
+
key = self.ar_table.dereference()['pairs'][i]['key']
|
|
111
|
+
# Skip undefined/deleted entries
|
|
112
|
+
if int(key) != RUBY_Qundef:
|
|
113
|
+
value = self.ar_table.dereference()['pairs'][i]['val']
|
|
114
|
+
yield (key, value)
|
|
115
|
+
|
|
116
|
+
def __str__(self):
|
|
117
|
+
"""Return string representation of hash."""
|
|
118
|
+
addr = int(self.value)
|
|
119
|
+
return f"<T_HASH@0x{addr:x} AR-Table size={self.size()} bound={self.bound()}>"
|
|
120
|
+
|
|
121
|
+
def print_to(self, terminal):
|
|
122
|
+
"""Print this hash with formatting."""
|
|
123
|
+
addr = int(self.value)
|
|
124
|
+
return terminal.print(
|
|
125
|
+
format.metadata, '<',
|
|
126
|
+
format.type, 'T_HASH',
|
|
127
|
+
format.metadata, f'@0x{addr:x} AR-Table size={self.size()} bound={self.bound()}>',
|
|
128
|
+
format.reset
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def print_recursive(self, printer, depth):
|
|
132
|
+
"""Print this hash recursively."""
|
|
133
|
+
printer.print(self)
|
|
134
|
+
|
|
135
|
+
if depth <= 0:
|
|
136
|
+
if self.size() > 0:
|
|
137
|
+
printer.print_with_indent(printer.max_depth - depth, " ...")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Print each key-value pair
|
|
141
|
+
for i, (key, value) in enumerate(self.pairs()):
|
|
142
|
+
printer.print_key_label(printer.max_depth - depth, i)
|
|
143
|
+
printer.print_value(key, depth - 1)
|
|
144
|
+
printer.print_value_label(printer.max_depth - depth)
|
|
145
|
+
printer.print_value(value, depth - 1)
|
|
146
|
+
|
|
147
|
+
def RHash(value):
|
|
148
|
+
"""Factory function that returns the appropriate RHash variant.
|
|
149
|
+
|
|
150
|
+
Caller should ensure value is a RUBY_T_HASH before calling this function.
|
|
151
|
+
"""
|
|
152
|
+
# Get flags to determine ST table vs AR table
|
|
153
|
+
basic = value.cast(constants.type_struct('struct RBasic').pointer())
|
|
154
|
+
flags = int(basic.dereference()['flags'])
|
|
155
|
+
|
|
156
|
+
if (flags & constants.get("RHASH_ST_TABLE_FLAG")) != 0:
|
|
157
|
+
return RHashSTTable(value)
|
|
158
|
+
else:
|
|
159
|
+
return RHashARTable(value)
|