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,479 @@
1
+ class Arguments:
2
+ """Structured result from parsing GDB command arguments.
3
+
4
+ Attributes:
5
+ expressions: List of expressions to evaluate (e.g., ["$var", "$ec->cfp->sp[-1]"])
6
+ flags: Set of boolean flags (e.g., {'debug'})
7
+ options: Dict of options with values (e.g., {'depth': 3})
8
+ """
9
+ def __init__(self, expressions, flags, options):
10
+ self.expressions = expressions
11
+ self.flags = flags
12
+ self.options = options
13
+
14
+ def has_flag(self, flag_name):
15
+ """Check if a boolean flag is present"""
16
+ return flag_name in self.flags
17
+
18
+ def get_option(self, option_name, default=None):
19
+ """Get an option value with optional default"""
20
+ return self.options.get(option_name, default)
21
+
22
+
23
+ class Usage:
24
+ """Command specification DSL for declarative command interfaces.
25
+
26
+ Defines what parameters, options, and flags a command accepts,
27
+ enabling validation and help text generation.
28
+
29
+ Example:
30
+ usage = Usage(
31
+ summary="Print Ruby objects with recursion",
32
+ parameters=['value'],
33
+ options={'depth': (int, 1), 'limit': (int, None)},
34
+ flags=['debug', 'verbose']
35
+ )
36
+
37
+ arguments = usage.parse("$var --depth 3 --debug")
38
+ # arguments.expressions = ['$var']
39
+ # arguments.options = {'depth': 3}
40
+ # arguments.flags = {'debug'}
41
+ """
42
+
43
+ def __init__(self, summary, parameters=None, options=None, flags=None, examples=None):
44
+ """Define command interface.
45
+
46
+ Args:
47
+ summary: One-line command description
48
+ parameters: List of parameter names or tuples (name, description)
49
+ options: Dict of {name: (type, default, description)}
50
+ flags: List of flag names or tuples (name, description)
51
+ examples: List of example command strings with descriptions
52
+
53
+ Example:
54
+ Usage(
55
+ summary="Scan heap for objects",
56
+ parameters=[('type_name', 'Ruby type to search for')],
57
+ options={
58
+ 'limit': (int, None, 'Maximum objects to find'),
59
+ 'depth': (int, 1, 'Recursion depth')
60
+ },
61
+ flags=[
62
+ ('terminated', 'Include terminated fibers'),
63
+ ('cache', 'Use cached results')
64
+ ],
65
+ examples=[
66
+ ("rb-heap-scan --type RUBY_T_STRING", "Find all strings"),
67
+ ("rb-heap-scan --type RUBY_T_HASH --limit 10", "Find first 10 hashes")
68
+ ]
69
+ )
70
+ """
71
+ self.summary = summary
72
+ self.parameters = self._normalize_params(parameters or [])
73
+ self.options = options or {}
74
+ self.flags = self._normalize_flags(flags or [])
75
+ self.examples = examples or []
76
+
77
+ def _normalize_params(self, params):
78
+ """Normalize parameters to list of (name, description) tuples."""
79
+ normalized = []
80
+ for param in params:
81
+ if isinstance(param, tuple):
82
+ normalized.append(param)
83
+ else:
84
+ normalized.append((param, None))
85
+ return normalized
86
+
87
+ def _normalize_flags(self, flags):
88
+ """Normalize flags to list of (name, description) tuples."""
89
+ normalized = []
90
+ for flag in flags:
91
+ if isinstance(flag, tuple):
92
+ normalized.append(flag)
93
+ else:
94
+ normalized.append((flag, None))
95
+ return normalized
96
+
97
+ def parse(self, argument_string):
98
+ """Parse and validate command arguments.
99
+
100
+ Args:
101
+ argument_string: Raw argument string from debugger
102
+
103
+ Returns:
104
+ Arguments object with validated and type-converted values
105
+
106
+ Raises:
107
+ ValueError: If validation fails (wrong parameter count, invalid types, etc.)
108
+ """
109
+ # Use existing parser to extract raw arguments
110
+ arguments = parse_arguments(argument_string)
111
+
112
+ # Validate parameter count
113
+ if len(arguments.expressions) != len(self.parameters):
114
+ if len(self.parameters) == 0 and len(arguments.expressions) > 0:
115
+ raise ValueError(f"Command takes no parameters, got {len(arguments.expressions)}")
116
+ elif len(self.parameters) == 1:
117
+ raise ValueError(f"Command requires 1 parameter, got {len(arguments.expressions)}")
118
+ else:
119
+ raise ValueError(f"Command requires {len(self.parameters)} parameters, got {len(arguments.expressions)}")
120
+
121
+ # Validate and convert option types
122
+ converted_options = {}
123
+ for option_name, option_value in arguments.options.items():
124
+ if option_name not in self.options:
125
+ raise ValueError(f"Unknown option: --{option_name}")
126
+
127
+ # Unpack option spec (handle 2-tuple or 3-tuple)
128
+ opt_spec = self.options[option_name]
129
+ option_type = opt_spec[0]
130
+ option_default = opt_spec[1]
131
+
132
+ # Convert to specified type
133
+ try:
134
+ if option_type == int:
135
+ converted_options[option_name] = int(option_value)
136
+ elif option_type == str:
137
+ converted_options[option_name] = str(option_value)
138
+ elif option_type == bool:
139
+ converted_options[option_name] = bool(option_value)
140
+ else:
141
+ # Custom type converter
142
+ converted_options[option_name] = option_type(option_value)
143
+ except (ValueError, TypeError) as e:
144
+ raise ValueError(f"Invalid value for --{option_name}: {option_value} (expected {option_type.__name__})")
145
+
146
+ # Add defaults for missing options
147
+ for option_name, opt_spec in self.options.items():
148
+ option_default = opt_spec[1]
149
+ if option_name not in converted_options and option_default is not None:
150
+ converted_options[option_name] = option_default
151
+
152
+ # Validate flags
153
+ flag_names = {flag[0] for flag in self.flags}
154
+ for flag_name in arguments.flags:
155
+ if flag_name not in flag_names:
156
+ raise ValueError(f"Unknown flag: --{flag_name}")
157
+
158
+ # Return new Arguments with converted options
159
+ return Arguments(arguments.expressions, arguments.flags, converted_options)
160
+
161
+ def print_to(self, terminal, command_name):
162
+ """Print help text from usage specification.
163
+
164
+ Args:
165
+ terminal: Terminal for colored output
166
+ command_name: Name of the command (e.g., "rb-print")
167
+ """
168
+ import format as fmt
169
+
170
+ # Summary with color
171
+ terminal.print(fmt.bold, self.summary, fmt.reset)
172
+ terminal.print()
173
+
174
+ # Print usage line
175
+ terminal.print("Usage: ", end='')
176
+ terminal.print(fmt.bold, command_name, fmt.reset, end='')
177
+
178
+ for param_name, _ in self.parameters:
179
+ terminal.print(' ', end='')
180
+ terminal.print(fmt.placeholder, f"<{param_name}>", fmt.reset, end='')
181
+
182
+ # Add option placeholders
183
+ for option_name in self.options.keys():
184
+ terminal.print(' ', end='')
185
+ terminal.print(fmt.placeholder, f"[--{option_name} N]", fmt.reset, end='')
186
+
187
+ # Add flag placeholders
188
+ for flag_name, _ in self.flags:
189
+ terminal.print(' ', end='')
190
+ terminal.print(fmt.placeholder, f"[--{flag_name}]", fmt.reset, end='')
191
+
192
+ terminal.print()
193
+ terminal.print()
194
+
195
+ # Parameter descriptions
196
+ if self.parameters:
197
+ terminal.print(fmt.title, "Parameters:", fmt.reset)
198
+
199
+ for param_name, param_desc in self.parameters:
200
+ terminal.print(" ", fmt.symbol, param_name, fmt.reset, end='')
201
+ if param_desc:
202
+ terminal.print(f" - {param_desc}")
203
+ else:
204
+ terminal.print()
205
+ terminal.print()
206
+
207
+ # Option descriptions
208
+ if self.options:
209
+ terminal.print(fmt.title, "Options:", fmt.reset)
210
+
211
+ for option_name, opt_spec in self.options.items():
212
+ opt_type, opt_default = opt_spec[0], opt_spec[1]
213
+ opt_desc = opt_spec[2] if len(opt_spec) > 2 else None
214
+
215
+ type_str = opt_type.__name__ if hasattr(opt_type, '__name__') else str(opt_type)
216
+ default_str = f" (default: {opt_default})" if opt_default is not None else ""
217
+
218
+ terminal.print(" ", fmt.symbol, f"--{option_name}", fmt.reset, end='')
219
+ terminal.print(fmt.placeholder, f" <{type_str}>", fmt.reset, end='')
220
+ terminal.print(default_str)
221
+
222
+ if opt_desc:
223
+ terminal.print(f" {opt_desc}")
224
+ terminal.print()
225
+
226
+ # Flag descriptions
227
+ if self.flags:
228
+ terminal.print(fmt.title, "Flags:", fmt.reset)
229
+
230
+ for flag_name, flag_desc in self.flags:
231
+ terminal.print(" ", fmt.symbol, f"--{flag_name}", fmt.reset, end='')
232
+ if flag_desc:
233
+ terminal.print(f" - {flag_desc}")
234
+ else:
235
+ terminal.print()
236
+ terminal.print()
237
+
238
+ # Examples section
239
+ if self.examples:
240
+ terminal.print(fmt.title, "Examples:", fmt.reset)
241
+
242
+ for example_cmd, example_desc in self.examples:
243
+ terminal.print(fmt.example, f" {example_cmd}", fmt.reset)
244
+ if example_desc:
245
+ terminal.print(f" {example_desc}")
246
+
247
+ class ArgumentParser:
248
+ """Parse GDB command arguments handling nested brackets, quotes, and flags.
249
+
250
+ This parser correctly handles:
251
+ - Nested brackets: $ec->cfp->sp[-1]
252
+ - Nested parentheses: ((struct foo*)bar)->baz
253
+ - Quotes: "string value"
254
+ - Flags: --debug, --depth 3
255
+ - Complex expressions: foo + 10, bar->ptr[idx * 2]
256
+ """
257
+
258
+ def __init__(self, argument_string):
259
+ self.argument_string = argument_string.strip()
260
+ self.position = 0
261
+ self.length = len(self.argument_string)
262
+
263
+ def parse(self):
264
+ """Parse the argument string and return (expressions, flags, options)
265
+
266
+ Returns:
267
+ tuple: (expressions: list[str], flags: set, options: dict)
268
+ - expressions: List of expressions to evaluate
269
+ - flags: Set of boolean flags (e.g., {'debug'})
270
+ - options: Dict of options with values (e.g., {'depth': 3})
271
+ """
272
+ flags = set()
273
+ options = {}
274
+ expressions = []
275
+
276
+ while self.position < self.length:
277
+ self.skip_whitespace()
278
+ if self.position >= self.length:
279
+ break
280
+
281
+ if self.peek() == '-' and self.peek(1) == '-':
282
+ # Parse a flag or option
283
+ flag_name, flag_value = self.parse_flag()
284
+ if flag_value is None:
285
+ flags.add(flag_name)
286
+ else:
287
+ options[flag_name] = flag_value
288
+ else:
289
+ # Parse an expression
290
+ expression = self.parse_expression()
291
+ if expression:
292
+ expressions.append(expression)
293
+
294
+ return expressions, flags, options
295
+
296
+ def peek(self, offset=0):
297
+ """Peek at character at current position + offset"""
298
+ position = self.position + offset
299
+ if position < self.length:
300
+ return self.argument_string[position]
301
+ return None
302
+
303
+ def consume(self, count=1):
304
+ """Consume and return count characters"""
305
+ result = self.argument_string[self.position:self.position + count]
306
+ self.position += count
307
+ return result
308
+
309
+ def skip_whitespace(self):
310
+ """Skip whitespace characters"""
311
+ while self.position < self.length and self.argument_string[self.position].isspace():
312
+ self.position += 1
313
+
314
+ def parse_flag(self):
315
+ """Parse a flag starting with --
316
+
317
+ Returns:
318
+ tuple: (flag_name: str, value: str|None)
319
+ - For boolean flags: ('debug', None)
320
+ - For valued flags: ('depth', '3')
321
+ """
322
+ # Consume '--'
323
+ self.consume(2)
324
+
325
+ # Read flag name
326
+ flag_name = ''
327
+ while self.position < self.length and self.argument_string[self.position].isalnum():
328
+ flag_name += self.consume()
329
+
330
+ # Check if flag has a value
331
+ self.skip_whitespace()
332
+
333
+ # If next character is not another flag and not end of string, it might be a value
334
+ if self.position < self.length and not (self.peek() == '-' and self.peek(1) == '-'):
335
+ # Try to parse a value - it could start with $, digit, or letter
336
+ if self.peek() and not self.peek().isspace():
337
+ value = ''
338
+ while self.position < self.length and not self.argument_string[self.position].isspace():
339
+ if self.peek() == '-' and self.peek(1) == '-':
340
+ break
341
+ value += self.consume()
342
+
343
+ # Try to convert to int if it's a number
344
+ try:
345
+ return flag_name, int(value)
346
+ except ValueError:
347
+ return flag_name, value
348
+
349
+ return flag_name, None
350
+
351
+ def parse_expression(self):
352
+ """Parse a single expression, stopping at whitespace (unless nested) or flags.
353
+
354
+ An expression can be:
355
+ - A quoted string: "foo" or 'bar'
356
+ - A parenthesized expression: (x + y)
357
+ - A variable with accessors: $ec->cfp->sp[-1]
358
+ - Any combination that doesn't contain unquoted/unnested whitespace
359
+ """
360
+ expression = ''
361
+
362
+ while self.position < self.length:
363
+ self.skip_whitespace()
364
+ if self.position >= self.length:
365
+ break
366
+
367
+ character = self.peek()
368
+
369
+ # Stop at flags
370
+ if character == '-' and self.peek(1) == '-':
371
+ break
372
+
373
+ # Handle quoted strings - these are complete expressions
374
+ if character in ('"', "'"):
375
+ quoted = self.parse_quoted_string(character)
376
+ expression += quoted
377
+ # After a quoted string, we're done with this expression
378
+ break
379
+
380
+ # Handle parentheses - collect the whole balanced expression
381
+ if character == '(':
382
+ expression += self.parse_balanced('(', ')')
383
+ continue
384
+
385
+ # Handle brackets
386
+ if character == '[':
387
+ expression += self.parse_balanced('[', ']')
388
+ continue
389
+
390
+ # Handle braces
391
+ if character == '{':
392
+ expression += self.parse_balanced('{', '}')
393
+ continue
394
+
395
+ # Stop at whitespace (this separates expressions)
396
+ if character.isspace():
397
+ break
398
+
399
+ # Regular character - part of a variable name, operator, etc.
400
+ expression += self.consume()
401
+
402
+ return expression.strip()
403
+
404
+ def parse_quoted_string(self, quote_character):
405
+ """Parse a quoted string, handling escapes"""
406
+ result = self.consume() # Opening quote
407
+
408
+ while self.position < self.length:
409
+ character = self.peek()
410
+
411
+ if character == '\\':
412
+ # Escape sequence
413
+ result += self.consume()
414
+ if self.position < self.length:
415
+ result += self.consume()
416
+ elif character == quote_character:
417
+ # Closing quote
418
+ result += self.consume()
419
+ break
420
+ else:
421
+ result += self.consume()
422
+
423
+ return result
424
+
425
+ def parse_balanced(self, open_character, close_character):
426
+ """Parse a balanced pair of delimiters (e.g., parentheses, brackets)"""
427
+ result = self.consume() # Opening delimiter
428
+ depth = 1
429
+
430
+ while self.position < self.length and depth > 0:
431
+ character = self.peek()
432
+
433
+ # Handle quotes inside balanced delimiters
434
+ if character in ('"', "'"):
435
+ result += self.parse_quoted_string(character)
436
+ continue
437
+
438
+ if character == open_character:
439
+ depth += 1
440
+ elif character == close_character:
441
+ depth -= 1
442
+
443
+ result += self.consume()
444
+
445
+ return result
446
+
447
+ def parse_arguments(input):
448
+ """Convenience function to parse argument string.
449
+
450
+ Arguments:
451
+ input: The raw argument string from GDB command
452
+
453
+ Returns:
454
+ Arguments: Structured object with expressions, flags, and options
455
+
456
+ Examples:
457
+ >>> arguments = parse_arguments("$var --debug")
458
+ >>> arguments.expressions
459
+ ['$var']
460
+ >>> arguments.has_flag('debug')
461
+ True
462
+
463
+ >>> arguments = parse_arguments('"foo" "bar" --depth 3')
464
+ >>> arguments.expressions
465
+ ['"foo"', '"bar"']
466
+ >>> arguments.get_option('depth')
467
+ 3
468
+
469
+ >>> arguments = parse_arguments("$ec->cfp->sp[-1] --debug --depth 2")
470
+ >>> arguments.expressions
471
+ ['$ec->cfp->sp[-1]']
472
+ >>> arguments.has_flag('debug')
473
+ True
474
+ >>> arguments.get_option('depth')
475
+ 2
476
+ """
477
+ parser = ArgumentParser(input)
478
+ expressions, flags, options = parser.parse()
479
+ return Arguments(expressions, flags, options)
@@ -0,0 +1,200 @@
1
+ import debugger
2
+
3
+ # Global shared cache for Ruby constants
4
+ _CACHE = {}
5
+
6
+ # Global shared cache for debugger type lookups
7
+ _TYPE_CACHE = {}
8
+
9
+ # Default values for Ruby type constants (ruby_value_type enum)
10
+ _TYPE_DEFAULTS = {
11
+ 'RUBY_T_NONE': 0x00,
12
+ 'RUBY_T_OBJECT': 0x01,
13
+ 'RUBY_T_CLASS': 0x02,
14
+ 'RUBY_T_MODULE': 0x03,
15
+ 'RUBY_T_FLOAT': 0x04,
16
+ 'RUBY_T_STRING': 0x05,
17
+ 'RUBY_T_REGEXP': 0x06,
18
+ 'RUBY_T_ARRAY': 0x07,
19
+ 'RUBY_T_HASH': 0x08,
20
+ 'RUBY_T_STRUCT': 0x09,
21
+ 'RUBY_T_BIGNUM': 0x0a,
22
+ 'RUBY_T_FILE': 0x0b,
23
+ 'RUBY_T_DATA': 0x0c,
24
+ 'RUBY_T_MATCH': 0x0d,
25
+ 'RUBY_T_COMPLEX': 0x0e,
26
+ 'RUBY_T_RATIONAL': 0x0f,
27
+ 'RUBY_T_NIL': 0x11,
28
+ 'RUBY_T_TRUE': 0x12,
29
+ 'RUBY_T_FALSE': 0x13,
30
+ 'RUBY_T_SYMBOL': 0x14,
31
+ 'RUBY_T_FIXNUM': 0x15,
32
+ 'RUBY_T_UNDEF': 0x16,
33
+ 'RUBY_T_IMEMO': 0x1a,
34
+ 'RUBY_T_NODE': 0x1b,
35
+ 'RUBY_T_ICLASS': 0x1c,
36
+ 'RUBY_T_ZOMBIE': 0x1d,
37
+ 'RUBY_T_MOVED': 0x1e,
38
+ 'RUBY_T_MASK': 0x1f,
39
+ }
40
+
41
+ # Default values for Ruby flag constants (ruby_fl_type enum)
42
+ _FLAG_DEFAULTS = {
43
+ 'RUBY_FL_USHIFT': 12,
44
+ 'RUBY_FL_USER1': 1 << 13,
45
+ 'RUBY_FL_USER2': 1 << 14,
46
+ 'RUBY_FL_USER3': 1 << 15,
47
+ 'RUBY_FL_USER4': 1 << 16,
48
+ 'RUBY_FL_USER5': 1 << 17,
49
+ 'RUBY_FL_USER6': 1 << 18,
50
+ 'RUBY_FL_USER7': 1 << 19,
51
+ 'RUBY_FL_USER8': 1 << 20,
52
+ 'RUBY_FL_USER9': 1 << 21,
53
+ }
54
+
55
+ def get(name, default=None):
56
+ """Get a Ruby constant value, with caching.
57
+
58
+ Arguments:
59
+ name: The constant name (e.g., 'RUBY_T_STRING', 'RUBY_FL_USER1')
60
+ default: Default value if constant cannot be found
61
+
62
+ Returns:
63
+ The integer value of the constant, or default if not found
64
+
65
+ Raises:
66
+ Exception if constant not found and no default provided
67
+ """
68
+ if name in _CACHE:
69
+ return _CACHE[name]
70
+
71
+ # Try direct evaluation (works in GDB with debug info, LLDB with macros/variables)
72
+ try:
73
+ val = int(debugger.parse_and_eval(name))
74
+ # Only cache if we got a non-zero value or if it's a known zero constant
75
+ if val != 0 or name in ['Qfalse', 'RUBY_Qfalse', 'RUBY_T_NONE']:
76
+ _CACHE[name] = val
77
+ return val
78
+ except Exception:
79
+ pass
80
+
81
+ # Couldn't find the constant
82
+ if default is None:
83
+ raise Exception(f"Constant {name} not found and no default provided")
84
+
85
+ # Return default but don't cache (might be available later with a process)
86
+ return default
87
+
88
+ def get_enum(enum_name, member_name, default=None):
89
+ """Get an enum member value from a specific enum.
90
+
91
+ This is more explicit than get() and works better in LLDB.
92
+
93
+ Arguments:
94
+ enum_name: The enum type name (e.g., 'ruby_value_type')
95
+ member_name: The member name (e.g., 'RUBY_T_STRING')
96
+ default: Default value if enum member cannot be found
97
+
98
+ Returns:
99
+ The integer value of the enum member, or default if not found
100
+
101
+ Raises:
102
+ Exception if member not found and no default provided
103
+
104
+ Examples:
105
+ >>> constants.get_enum('ruby_value_type', 'RUBY_T_STRING', 0x05)
106
+ 5
107
+ """
108
+ cache_key = f"{enum_name}::{member_name}"
109
+
110
+ if cache_key in _CACHE:
111
+ return _CACHE[cache_key]
112
+
113
+ # Use the debugger abstraction (handles GDB vs LLDB differences)
114
+ try:
115
+ val = debugger.get_enum_value(enum_name, member_name)
116
+ _CACHE[cache_key] = val
117
+ return val
118
+ except Exception:
119
+ pass
120
+
121
+ # Couldn't find the enum member
122
+ if default is None:
123
+ raise Exception(f"Enum member {enum_name}::{member_name} not found and no default provided")
124
+
125
+ return default
126
+
127
+ def type_struct(type_name):
128
+ """Get a C struct/type from the debugger, with caching.
129
+
130
+ Arguments:
131
+ type_name: The type name (e.g., 'struct RString', 'struct RArray', 'VALUE')
132
+
133
+ Returns:
134
+ The debugger type object
135
+
136
+ Raises:
137
+ Exception if type cannot be found
138
+
139
+ Examples:
140
+ >>> rbasic_type = constants.type_struct('struct RBasic')
141
+ >>> value_type = constants.type_struct('VALUE')
142
+ """
143
+ if type_name in _TYPE_CACHE:
144
+ return _TYPE_CACHE[type_name]
145
+
146
+ dbg_type = debugger.lookup_type(type_name)
147
+ _TYPE_CACHE[type_name] = dbg_type
148
+ return dbg_type
149
+
150
+ def type(name):
151
+ """Get a Ruby type constant (RUBY_T_*) value.
152
+
153
+ This is a convenience wrapper around get_enum() for ruby_value_type enum.
154
+ Uses built-in defaults for all standard Ruby type constants.
155
+
156
+ Arguments:
157
+ name: The type constant name (e.g., 'RUBY_T_STRING', 'RUBY_T_ARRAY')
158
+
159
+ Returns:
160
+ The integer value of the type constant
161
+
162
+ Raises:
163
+ Exception if constant is not found and not in default table
164
+
165
+ Examples:
166
+ >>> constants.type('RUBY_T_STRING')
167
+ 5
168
+ """
169
+ default = _TYPE_DEFAULTS.get(name)
170
+ return get_enum('ruby_value_type', name, default)
171
+
172
+ def flag(name):
173
+ """Get a Ruby flag constant (RUBY_FL_*) value.
174
+
175
+ This is a convenience wrapper around get_enum() for ruby_fl_type enum.
176
+ Uses built-in defaults for all standard Ruby flag constants.
177
+
178
+ Arguments:
179
+ name: The flag constant name (e.g., 'RUBY_FL_USER1', 'RUBY_FL_USHIFT')
180
+
181
+ Returns:
182
+ The integer value of the flag constant
183
+
184
+ Raises:
185
+ Exception if constant is not found and not in default table
186
+
187
+ Examples:
188
+ >>> constants.flag('RUBY_FL_USER1')
189
+ 8192
190
+ """
191
+ default = _FLAG_DEFAULTS.get(name)
192
+ return get_enum('ruby_fl_type', name, default)
193
+
194
+ def clear():
195
+ """Clear the constants and type caches.
196
+
197
+ Useful when switching between different Ruby processes or versions.
198
+ """
199
+ _CACHE.clear()
200
+ _TYPE_CACHE.clear()