toolbox 0.2.0 → 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.
@@ -4,627 +4,617 @@ import debugger
4
4
  import sys
5
5
 
6
6
  # Import Ruby GDB modules
7
+ import command
8
+ import constants
7
9
  import context
8
10
  import format
9
- import value
11
+ import rvalue
10
12
  import rstring
11
13
  import rexception
12
14
  import rsymbol
13
15
 
14
16
 
15
17
  def print_fiber_backtrace(fiber_ptr, from_tty=True):
16
- """Print backtrace for a Ruby fiber.
17
-
18
- Args:
19
- fiber_ptr: Fiber struct pointer (rb_fiber_struct *)
20
- from_tty: Whether output is to terminal (for formatting)
21
- """
22
- printer = RubyStackPrinter()
23
- printer.print_fiber_backtrace(fiber_ptr, from_tty)
18
+ """Print backtrace for a Ruby fiber.
19
+
20
+ Args:
21
+ fiber_ptr: Fiber struct pointer (rb_fiber_struct *)
22
+ from_tty: Whether output is to terminal (for formatting)
23
+ """
24
+ printer = RubyStackPrinter()
25
+ printer.print_fiber_backtrace(fiber_ptr, from_tty)
24
26
 
25
27
 
26
28
  def print_ec_backtrace(ec, from_tty=True):
27
- """Print backtrace for an execution context.
28
-
29
- Args:
30
- ec: Execution context pointer (rb_execution_context_t *)
31
- from_tty: Whether output is to terminal (for formatting)
32
- """
33
- printer = RubyStackPrinter()
34
- printer.print_backtrace(ec, from_tty)
29
+ """Print backtrace for an execution context.
30
+
31
+ Args:
32
+ ec: Execution context pointer (rb_execution_context_t *)
33
+ from_tty: Whether output is to terminal (for formatting)
34
+ """
35
+ printer = RubyStackPrinter()
36
+ printer.print_backtrace(ec, from_tty)
35
37
 
36
38
 
37
39
  class RubyStackPrinter:
38
- """Helper class for printing Ruby stack traces.
39
-
40
- This class provides the core logic for printing backtraces that can be
41
- used both by commands and programmatically from other modules.
42
- """
43
-
44
- def __init__(self):
45
- # Cached type lookups
46
- self._rbasic_type = None
47
- self._value_type = None
48
- self._cfp_type = None
49
- self._rstring_type = None
50
- self.show_values = False
51
- self.terminal = None
52
-
53
- def _initialize_types(self):
54
- """Initialize cached type lookups."""
55
- if self._rbasic_type is None:
56
- self._rbasic_type = debugger.lookup_type('struct RBasic').pointer()
57
- if self._value_type is None:
58
- self._value_type = debugger.lookup_type('VALUE')
59
- if self._cfp_type is None:
60
- self._cfp_type = debugger.lookup_type('rb_control_frame_t').pointer()
61
- if self._rstring_type is None:
62
- self._rstring_type = debugger.lookup_type('struct RString').pointer()
63
-
64
- def print_fiber_backtrace(self, fiber_ptr, from_tty=True):
65
- """Print backtrace for a Ruby fiber.
66
-
67
- Args:
68
- fiber_ptr: Fiber struct pointer (rb_fiber_struct *)
69
- from_tty: Whether output is to terminal (for formatting)
70
- """
71
- try:
72
- self._initialize_types()
73
- self.terminal = format.create_terminal(from_tty)
74
-
75
- # Get execution context from fiber
76
- ec = fiber_ptr['cont']['saved_ec'].address
77
-
78
- print(f"Backtrace for fiber {fiber_ptr}:")
79
- self.print_backtrace(ec, from_tty)
80
-
81
- except (debugger.Error, RuntimeError) as e:
82
- print(f"Error printing fiber backtrace: {e}")
83
-
84
- def print_backtrace(self, ec, from_tty=True):
85
- """Print backtrace for an execution context.
86
-
87
- Args:
88
- ec: Execution context pointer (rb_execution_context_t *)
89
- from_tty: Whether output is to terminal (for formatting)
90
- """
91
- try:
92
- self._initialize_types()
93
- if self.terminal is None:
94
- self.terminal = format.create_terminal(from_tty)
95
-
96
- cfp = ec['cfp']
97
- vm_stack = ec['vm_stack']
98
- vm_stack_size = int(ec['vm_stack_size'])
99
-
100
- # Check for exception
101
- errinfo_val = ec['errinfo']
102
- errinfo_int = int(errinfo_val)
103
-
104
- # Check if it's a real exception object (not nil or other immediate/special value)
105
- if not value.is_immediate(errinfo_val) and not value.is_nil(errinfo_val):
106
- try:
107
- exc_class = self._get_exception_class(errinfo_val)
108
- exc_msg = self._get_exception_message(errinfo_val)
109
-
110
- # Set as GDB convenience variable for manual inspection
111
- debugger.set_convenience_variable('errinfo', errinfo_val)
112
-
113
- if exc_msg:
114
- print(f"Currently handling: {exc_class}: {exc_msg} (VALUE: 0x{errinfo_int:x}, $errinfo)")
115
- else:
116
- print(f"Currently handling: {exc_class} (VALUE: 0x{errinfo_int:x}, $errinfo)")
117
- print()
118
- except:
119
- # Set convenience variable even if we can't decode
120
- debugger.set_convenience_variable('errinfo', errinfo_val)
121
- print(f"Currently handling exception (VALUE: 0x{errinfo_int:x}, $errinfo)")
122
- print()
123
-
124
- # Calculate end of control frames
125
- cfpend = (vm_stack + vm_stack_size).cast(self._cfp_type) - 1
126
-
127
- frame_num = 0
128
- current_cfp = cfp
129
-
130
- while current_cfp < cfpend:
131
- try:
132
- self._print_frame(current_cfp, frame_num)
133
- frame_num += 1
134
- current_cfp += 1
135
- except (debugger.Error, RuntimeError) as e:
136
- print(f" #{frame_num}: [error reading frame: {e}]")
137
- break
138
-
139
- if frame_num == 0:
140
- print(" (no frames)")
141
-
142
- except (debugger.Error, RuntimeError) as e:
143
- print(f"Error printing backtrace: {e}")
144
-
145
- def _print_frame(self, cfp, depth):
146
- """Print a single control frame.
147
-
148
- Args:
149
- cfp: Control frame pointer (rb_control_frame_t *)
150
- depth: Frame depth/number
151
- """
152
- iseq = cfp['iseq']
153
-
154
- if iseq is None or int(iseq) == 0:
155
- # C function frame - try to extract method info from EP
156
- try:
157
- ep = cfp['ep']
158
-
159
- # Check if this is a valid C frame
160
- if int(ep) != 0:
161
- ep0 = int(ep[0])
162
- if (ep0 & 0xffff0001) == 0x55550001:
163
- # Valid C frame, try to extract method entry
164
- env_me_cref = ep[-2]
165
-
166
- try:
167
- me_type = debugger.lookup_type('rb_callable_method_entry_t').pointer()
168
- me = env_me_cref.cast(me_type)
169
-
170
- # Get the C function pointer
171
- cfunc = me['def']['body']['cfunc']['func']
172
-
173
- # Get the method ID
174
- method_id = me['def']['original_id']
175
-
176
- # Try to get symbol for the C function
177
- func_addr = int(cfunc)
178
- func_name = debugger.lookup_symbol(func_addr)
179
- if func_name:
180
- func_name = f" ({func_name})"
181
- else:
182
- func_name = f" (0x{func_addr:x})"
183
-
184
- # Print C frame with cyan/dimmed formatting
185
- print(self.terminal.print(
186
- format.metadata, f" #{depth}: ",
187
- format.dim, "[C function", func_name, "]",
188
- format.reset
189
- ))
190
- return
191
- except:
192
- pass
193
- except:
194
- pass
195
-
196
- # Fallback if we couldn't extract info
197
- print(self.terminal.print(
198
- format.metadata, f" #{depth}: ",
199
- format.dim, "[C function or native frame]",
200
- format.reset
201
- ))
202
- return
203
-
204
- pc = cfp['pc']
205
-
206
- if int(pc) == 0:
207
- print(self.terminal.print(
208
- format.metadata, f" #{depth}: ",
209
- format.error, "???:???:in '???'",
210
- format.reset
211
- ))
212
- return
213
-
214
- # Check if it's an ifunc (internal function)
215
- RUBY_IMMEDIATE_MASK = 0x03
216
- RUBY_FL_USHIFT = 12
217
- RUBY_T_IMEMO = 0x1a
218
- RUBY_IMEMO_MASK = 0x0f
219
-
220
- iseq_val = int(iseq.cast(self._value_type))
221
- if not (iseq_val & RUBY_IMMEDIATE_MASK):
222
- try:
223
- flags = int(iseq['flags'])
224
- expected = ((RUBY_T_IMEMO << RUBY_FL_USHIFT) | RUBY_T_IMEMO)
225
- mask = ((RUBY_IMEMO_MASK << RUBY_FL_USHIFT) | 0x1f)
226
-
227
- if (flags & mask) == expected:
228
- # It's an ifunc
229
- print(self.terminal.print(
230
- format.metadata, f" #{depth}: ",
231
- format.dim, "[ifunc]",
232
- format.reset
233
- ))
234
- return
235
- except:
236
- pass
237
-
238
- try:
239
- # Get location information
240
- body = iseq['body']
241
- if body is None:
242
- print(self.terminal.print(
243
- format.metadata, f" #{depth}: ",
244
- format.error, "???:???:in '???'",
245
- format.reset
246
- ))
247
- return
248
-
249
- location = body['location']
250
- if location is None:
251
- print(self.terminal.print(
252
- format.metadata, f" #{depth}: ",
253
- format.error, "???:???:in '???'",
254
- format.reset
255
- ))
256
- return
257
-
258
- pathobj = location['pathobj']
259
- label = location['label']
260
-
261
- # Get path string - pathobj can be a string or an array [path, realpath]
262
- path = self._extract_path_from_pathobj(pathobj)
263
- label_str = self._value_to_string(label)
264
-
265
- # Calculate line number
266
- lineno = self._get_lineno(cfp)
267
-
268
- # Print Ruby frame with highlighting
269
- print(self.terminal.print(
270
- format.metadata, f" #{depth}: ",
271
- format.string, path,
272
- format.reset, ":",
273
- format.metadata, str(lineno),
274
- format.reset, ":in '",
275
- format.method, label_str,
276
- format.reset, "'"
277
- ))
278
-
279
- # If --values flag is set, print stack values
280
- if self.show_values:
281
- self._print_stack_values(cfp, iseq)
282
-
283
- except (debugger.Error, RuntimeError) as e:
284
- print(self.terminal.print(
285
- format.metadata, f" #{depth}: ",
286
- format.error, f"[error reading frame info: {e}]",
287
- format.reset
288
- ))
289
-
290
- def _print_stack_values(self, cfp, iseq):
291
- """Print Ruby VALUEs on the control frame's stack pointer.
292
-
293
- Args:
294
- cfp: Control frame pointer (rb_control_frame_t *)
295
- iseq: Instruction sequence pointer
296
- """
297
- try:
298
- sp = cfp['sp']
299
- ep = cfp['ep']
300
-
301
- if int(sp) == 0 or int(ep) == 0:
302
- return
303
-
304
- # Try to get local table information for better labeling
305
- local_names = []
306
- local_size = 0
307
- try:
308
- if int(iseq) != 0:
309
- iseq_body = iseq['body']
310
- local_table_size = int(iseq_body['local_table_size'])
311
-
312
- if local_table_size > 0:
313
- local_size = local_table_size
314
- local_table = iseq_body['local_table']
315
-
316
- # Read local variable names (they're stored as IDs/symbols)
317
- # Local table is stored in reverse order (last local first)
318
- for i in range(min(local_table_size, 20)): # Cap at 20
319
- try:
320
- local_id = local_table[local_table_size - 1 - i]
321
- # Try to convert ID to symbol name using RubySymbol
322
- if int(local_id) != 0:
323
- sym = rsymbol.RubySymbol(local_id)
324
- name = sym.to_str()
325
- if name:
326
- local_names.append(name)
327
- else:
328
- local_names.append(f"local_{i}")
329
- else:
330
- local_names.append(f"local_{i}")
331
- except:
332
- local_names.append(f"local_{i}")
333
- except:
334
- pass
335
-
336
- # Environment pointer typically points to the local variable area
337
- # Stack grows downward, so we start from sp and go down
338
- value_ptr = sp - 1
339
-
340
- # Print a reasonable number of stack values
341
- max_values = 10
342
- values_printed = 0
343
-
344
- print(self.terminal.print(format.dim, " Stack values:", format.reset))
345
-
346
- # Calculate offset from ep to show position
347
- while value_ptr >= ep and values_printed < max_values:
348
- try:
349
- val = value_ptr[0]
350
- val_int = int(val)
351
-
352
- # Calculate offset from ep for labeling
353
- offset = int(value_ptr - ep)
354
-
355
- # Try to determine if this is a local variable
356
- label = f"sp[-{values_printed + 1}]"
357
- if offset < local_size and offset < len(local_names):
358
- label = f"{local_names[offset]} (ep[{offset}])"
359
-
360
- # Try to get a brief representation of the value
361
- val_str = self._format_value_brief(val)
362
-
363
- print(self.terminal.print(
364
- format.metadata, f" {label:20s} ",
365
- format.dim, f"= ",
366
- format.reset, val_str,
367
- format.reset
368
- ))
369
-
370
- values_printed += 1
371
- value_ptr -= 1
372
- except (debugger.Error, debugger.MemoryError):
373
- break
374
-
375
- if values_printed == 0:
376
- print(self.terminal.print(format.dim, " (empty stack)", format.reset))
377
-
378
- except (debugger.Error, RuntimeError) as e:
379
- # Silently skip if we can't read stack values
380
- pass
381
-
382
- def _format_value_brief(self, val):
383
- """Get a brief string representation of a VALUE.
384
-
385
- Args:
386
- val: Ruby VALUE
387
-
388
- Returns:
389
- Brief string description
390
- """
391
- try:
392
- # Use value.py's interpret function to get the typed object
393
- obj = value.interpret(val)
394
-
395
- # Get string representation
396
- obj_str = str(obj)
397
-
398
- # Truncate if too long
399
- if len(obj_str) > 60:
400
- return obj_str[:57] + "..."
401
-
402
- return obj_str
403
-
404
- except Exception as e:
405
- return f"<error: {e}>"
406
-
407
- def _get_lineno(self, cfp):
408
- """Get line number for a control frame.
409
-
410
- Args:
411
- cfp: Control frame pointer
412
-
413
- Returns:
414
- Line number as int or "???" if unavailable
415
- """
416
- try:
417
- iseq = cfp['iseq']
418
- pc = cfp['pc']
419
-
420
- if int(pc) == 0:
421
- return "???"
422
-
423
- iseq_body = iseq['body']
424
- iseq_encoded = iseq_body['iseq_encoded']
425
- iseq_size = int(iseq_body['iseq_size'])
426
-
427
- pc_offset = int(pc - iseq_encoded)
428
-
429
- if pc_offset < 0 or pc_offset >= iseq_size:
430
- return "???"
431
-
432
- # Try to get line info
433
- insns_info = iseq_body['insns_info']
434
- positions = insns_info['positions']
435
-
436
- if int(positions) != 0:
437
- position = positions[pc_offset]
438
- lineno = int(position['lineno'])
439
- if lineno >= 0:
440
- return lineno
441
-
442
- # Fall back to first_lineno
443
- return int(iseq_body['location']['first_lineno'])
444
-
445
- except:
446
- return "???"
447
-
448
- def _get_exception_class(self, exc_value):
449
- """Get the class name of an exception object.
450
-
451
- Delegates to rexception.RException for proper exception handling.
452
-
453
- Args:
454
- exc_value: Exception VALUE
455
-
456
- Returns:
457
- Class name as string
458
- """
459
- try:
460
- exc = rexception.RException(exc_value)
461
- return exc.class_name
462
- except Exception:
463
- # Fallback if RException can't be created
464
- try:
465
- rbasic = exc_value.cast(self._rbasic_type)
466
- klass = rbasic['klass']
467
- return f"Exception(klass=0x{int(klass):x})"
468
- except:
469
- raise
470
-
471
- def _get_exception_message(self, exc_value):
472
- """Get the message from an exception object.
473
-
474
- Delegates to rexception.RException for proper exception handling.
475
-
476
- Args:
477
- exc_value: Exception VALUE
478
-
479
- Returns:
480
- Message string or None if unavailable
481
- """
482
- try:
483
- exc = rexception.RException(exc_value)
484
- return exc.message
485
- except Exception:
486
- # If RException can't be created, return None
487
- return None
488
-
489
- def _value_to_string(self, val):
490
- """Convert a Ruby VALUE to a Python string.
491
-
492
- Args:
493
- val: Ruby VALUE
494
-
495
- Returns:
496
- String representation
497
- """
498
- try:
499
- # Use the value.interpret infrastructure for proper type handling
500
- obj = value.interpret(val)
501
-
502
- # For strings, get the actual content
503
- if hasattr(obj, 'to_str'):
504
- return obj.to_str()
505
-
506
- # For immediates and other types, convert to string
507
- obj_str = str(obj)
508
-
509
- # Strip the type tag if present (e.g., "<T_FIXNUM> 42" -> "42")
510
- if obj_str.startswith('<'):
511
- # Find the end of the type tag
512
- end_tag = obj_str.find('>')
513
- if end_tag != -1 and end_tag + 2 < len(obj_str):
514
- # Return the part after the tag and space
515
- return obj_str[end_tag + 2:]
516
-
517
- return obj_str
518
-
519
- except Exception as e:
520
- return f"<error:{e}>"
521
-
522
- def _extract_path_from_pathobj(self, pathobj):
523
- """Extract file path from pathobj (can be string or array).
524
-
525
- Args:
526
- pathobj: Ruby VALUE (either T_STRING or T_ARRAY)
527
-
528
- Returns:
529
- File path as string
530
- """
531
- try:
532
- # Interpret the pathobj to get its type
533
- obj = value.interpret(pathobj)
534
-
535
- # If it's an array, get the first element (the path)
536
- if hasattr(obj, 'length') and hasattr(obj, 'get_item'):
537
- if obj.length() > 0:
538
- path_value = obj.get_item(0)
539
- return self._value_to_string(path_value)
540
-
541
- # Otherwise, treat it as a string directly
542
- return self._value_to_string(pathobj)
543
-
544
- except Exception as e:
545
- return f"<error:{e}>"
40
+ """Helper class for printing Ruby stack traces.
41
+
42
+ This class provides the core logic for printing backtraces that can be
43
+ used both by commands and programmatically from other modules.
44
+ """
45
+
46
+ def __init__(self):
47
+ # Cached type lookups
48
+ self._rbasic_type = None
49
+ self._value_type = None
50
+ self._cfp_type = None
51
+ self._rstring_type = None
52
+ self.show_values = False
53
+ self.terminal = None
54
+
55
+ def _initialize_types(self):
56
+ """Initialize cached type lookups."""
57
+ if self._rbasic_type is None:
58
+ self._rbasic_type = constants.type_struct('struct RBasic').pointer()
59
+ if self._value_type is None:
60
+ self._value_type = constants.type_struct('VALUE')
61
+ if self._cfp_type is None:
62
+ self._cfp_type = constants.type_struct('rb_control_frame_t').pointer()
63
+ if self._rstring_type is None:
64
+ self._rstring_type = constants.type_struct('struct RString').pointer()
65
+
66
+ def print_fiber_backtrace(self, fiber_ptr, from_tty=True):
67
+ """Print backtrace for a Ruby fiber.
68
+
69
+ Args:
70
+ fiber_ptr: Fiber struct pointer (rb_fiber_struct *)
71
+ from_tty: Whether output is to terminal (for formatting)
72
+ """
73
+ try:
74
+ self._initialize_types()
75
+ self.terminal = format.create_terminal(from_tty)
76
+
77
+ # Get execution context from fiber
78
+ ec = fiber_ptr['cont']['saved_ec'].address
79
+
80
+ print(f"Backtrace for fiber {fiber_ptr}:")
81
+ self.print_backtrace(ec, from_tty)
82
+
83
+ except (debugger.Error, RuntimeError) as e:
84
+ print(f"Error printing fiber backtrace: {e}")
85
+
86
+ def print_backtrace(self, ec, from_tty=True):
87
+ """Print backtrace for an execution context.
88
+
89
+ Args:
90
+ ec: Execution context pointer (rb_execution_context_t *)
91
+ from_tty: Whether output is to terminal (for formatting)
92
+ """
93
+ try:
94
+ self._initialize_types()
95
+ if self.terminal is None:
96
+ self.terminal = format.create_terminal(from_tty)
97
+
98
+ cfp = ec['cfp']
99
+ vm_stack = ec['vm_stack']
100
+ vm_stack_size = int(ec['vm_stack_size'])
101
+
102
+ # Check for exception
103
+ errinfo_val = ec['errinfo']
104
+ errinfo_int = int(errinfo_val)
105
+
106
+ # Check if it's a real exception object (not nil or other immediate/special value)
107
+ if not rvalue.is_immediate(errinfo_val) and not rvalue.is_nil(errinfo_val):
108
+ try:
109
+ exc_class = self._get_exception_class(errinfo_val)
110
+ exc_msg = self._get_exception_message(errinfo_val)
111
+
112
+ # Set as GDB convenience variable for manual inspection
113
+ debugger.set_convenience_variable('errinfo', errinfo_val)
114
+
115
+ if exc_msg:
116
+ print(f"Currently handling: {exc_class}: {exc_msg} (VALUE: 0x{errinfo_int:x}, $errinfo)")
117
+ else:
118
+ print(f"Currently handling: {exc_class} (VALUE: 0x{errinfo_int:x}, $errinfo)")
119
+ print()
120
+ except:
121
+ # Set convenience variable even if we can't decode
122
+ debugger.set_convenience_variable('errinfo', errinfo_val)
123
+ print(f"Currently handling exception (VALUE: 0x{errinfo_int:x}, $errinfo)")
124
+ print()
125
+
126
+ # Calculate end of control frames
127
+ cfpend = (vm_stack + vm_stack_size).cast(self._cfp_type) - 1
128
+
129
+ frame_num = 0
130
+ current_cfp = cfp
131
+
132
+ while current_cfp < cfpend:
133
+ try:
134
+ self._print_frame(current_cfp, frame_num)
135
+ frame_num += 1
136
+ current_cfp += 1
137
+ except (debugger.Error, RuntimeError) as e:
138
+ print(f" #{frame_num}: [error reading frame: {e}]")
139
+ break
140
+
141
+ if frame_num == 0:
142
+ print(" (no frames)")
143
+
144
+ except (debugger.Error, RuntimeError) as e:
145
+ print(f"Error printing backtrace: {e}")
146
+
147
+ def _print_frame(self, cfp, depth):
148
+ """Print a single control frame.
149
+
150
+ Args:
151
+ cfp: Control frame pointer (rb_control_frame_t *)
152
+ depth: Frame depth/number
153
+ """
154
+ iseq = cfp['iseq']
155
+
156
+ if iseq is None or int(iseq) == 0:
157
+ # C function frame - try to extract method info from EP
158
+ try:
159
+ ep = cfp['ep']
160
+
161
+ # Check if this is a valid C frame
162
+ if int(ep) != 0:
163
+ ep0 = int(ep[0])
164
+ if (ep0 & 0xffff0001) == 0x55550001:
165
+ # Valid C frame, try to extract method entry
166
+ env_me_cref = ep[-2]
167
+
168
+ try:
169
+ me_type = constants.type_struct('rb_callable_method_entry_t').pointer()
170
+ me = env_me_cref.cast(me_type)
171
+
172
+ # Get the C function pointer
173
+ cfunc = me['def']['body']['cfunc']['func']
174
+
175
+ # Get the method ID
176
+ method_id = me['def']['original_id']
177
+
178
+ # Try to get symbol for the C function
179
+ func_addr = int(cfunc)
180
+ func_name = debugger.lookup_symbol(func_addr)
181
+ if func_name:
182
+ func_name = f" ({func_name})"
183
+ else:
184
+ func_name = f" (0x{func_addr:x})"
185
+
186
+ # Print C frame with cyan/dimmed formatting
187
+ self.terminal.print(
188
+ format.metadata, f" #{depth}: ",
189
+ format.dim, "[C function", func_name, "]",
190
+ format.reset
191
+ )
192
+ return
193
+ except:
194
+ pass
195
+ except:
196
+ pass
197
+
198
+ # Fallback if we couldn't extract info
199
+ self.terminal.print(
200
+ format.metadata, f" #{depth}: ",
201
+ format.dim, "[C function or native frame]",
202
+ format.reset
203
+ )
204
+ return
205
+
206
+ pc = cfp['pc']
207
+
208
+ if int(pc) == 0:
209
+ self.terminal.print(
210
+ format.metadata, f" #{depth}: ",
211
+ format.error, "???:???:in '???'",
212
+ format.reset
213
+ )
214
+ return
215
+
216
+ # Check if it's an ifunc (internal function)
217
+ RUBY_IMMEDIATE_MASK = 0x03
218
+ RUBY_FL_USHIFT = 12
219
+ RUBY_T_IMEMO = 0x1a
220
+ RUBY_IMEMO_MASK = 0x0f
221
+
222
+ iseq_val = int(iseq.cast(self._value_type))
223
+ if not (iseq_val & RUBY_IMMEDIATE_MASK):
224
+ try:
225
+ flags = int(iseq['flags'])
226
+ expected = ((RUBY_T_IMEMO << RUBY_FL_USHIFT) | RUBY_T_IMEMO)
227
+ mask = ((RUBY_IMEMO_MASK << RUBY_FL_USHIFT) | 0x1f)
228
+
229
+ if (flags & mask) == expected:
230
+ # It's an ifunc
231
+ self.terminal.print(
232
+ format.metadata, f" #{depth}: ",
233
+ format.dim, "[ifunc]",
234
+ format.reset
235
+ )
236
+ return
237
+ except:
238
+ pass
239
+
240
+ try:
241
+ # Get location information
242
+ body = iseq['body']
243
+ if body is None:
244
+ self.terminal.print(
245
+ format.metadata, f" #{depth}: ",
246
+ format.error, "???:???:in '???'",
247
+ format.reset
248
+ )
249
+ return
250
+
251
+ location = body['location']
252
+ if location is None:
253
+ self.terminal.print(
254
+ format.metadata, f" #{depth}: ",
255
+ format.error, "???:???:in '???'",
256
+ format.reset
257
+ )
258
+ return
259
+
260
+ pathobj = location['pathobj']
261
+ label = location['label']
262
+
263
+ # Get path string - pathobj can be a string or an array [path, realpath]
264
+ path = self._extract_path_from_pathobj(pathobj)
265
+ label_str = self._value_to_string(label)
266
+
267
+ # Calculate line number
268
+ lineno = self._get_lineno(cfp)
269
+
270
+ # Print Ruby frame with highlighting
271
+ self.terminal.print(
272
+ format.metadata, f" #{depth}: ",
273
+ format.string, path,
274
+ format.reset, ":",
275
+ format.metadata, str(lineno),
276
+ format.reset, ":in '",
277
+ format.method, label_str,
278
+ format.reset, "'"
279
+ )
280
+
281
+ # If --values flag is set, print stack values
282
+ if self.show_values:
283
+ self._print_stack_values(cfp, iseq)
284
+
285
+ except (debugger.Error, RuntimeError) as e:
286
+ self.terminal.print(
287
+ format.metadata, f" #{depth}: ",
288
+ format.error, f"[error reading frame info: {e}]",
289
+ format.reset
290
+ )
291
+
292
+ def _print_stack_values(self, cfp, iseq):
293
+ """Print Ruby VALUEs on the control frame's stack pointer.
294
+
295
+ Args:
296
+ cfp: Control frame pointer (rb_control_frame_t *)
297
+ iseq: Instruction sequence pointer
298
+ """
299
+ try:
300
+ sp = cfp['sp']
301
+ ep = cfp['ep']
302
+
303
+ if int(sp) == 0 or int(ep) == 0:
304
+ return
305
+
306
+ # Try to get local table information for better labeling
307
+ local_names = []
308
+ local_size = 0
309
+ try:
310
+ if int(iseq) != 0:
311
+ iseq_body = iseq['body']
312
+ local_table_size = int(iseq_body['local_table_size'])
313
+
314
+ if local_table_size > 0:
315
+ local_size = local_table_size
316
+ local_table = iseq_body['local_table']
317
+
318
+ # Read local variable names (they're stored as IDs/symbols)
319
+ # Local table is stored in reverse order (last local first)
320
+ for i in range(min(local_table_size, 20)): # Cap at 20
321
+ try:
322
+ local_id = local_table[local_table_size - 1 - i]
323
+ # Try to convert ID to symbol name using RubySymbol
324
+ if int(local_id) != 0:
325
+ sym = rsymbol.RubySymbol(local_id)
326
+ name = sym.to_str()
327
+ if name:
328
+ local_names.append(name)
329
+ else:
330
+ local_names.append(f"local_{i}")
331
+ else:
332
+ local_names.append(f"local_{i}")
333
+ except:
334
+ local_names.append(f"local_{i}")
335
+ except:
336
+ pass
337
+
338
+ # Environment pointer typically points to the local variable area
339
+ # Stack grows downward, so we start from sp and go down
340
+ value_ptr = sp - 1
341
+
342
+ # Print a reasonable number of stack values
343
+ max_values = 10
344
+ values_printed = 0
345
+
346
+ self.terminal.print(format.dim, " Stack values:", format.reset)
347
+
348
+ # Calculate offset from ep to show position
349
+ while value_ptr >= ep and values_printed < max_values:
350
+ try:
351
+ val = value_ptr[0]
352
+ val_int = int(val)
353
+
354
+ # Calculate offset from ep for labeling
355
+ offset = int(value_ptr - ep)
356
+
357
+ # Try to determine if this is a local variable
358
+ label = f"sp[-{values_printed + 1}]"
359
+ if offset < local_size and offset < len(local_names):
360
+ label = f"{local_names[offset]} (ep[{offset}])"
361
+
362
+ # Try to get a brief representation of the value
363
+ val_str = self._format_value_brief(val)
364
+
365
+ self.terminal.print(
366
+ format.metadata, f" {label:20s} ",
367
+ format.dim, f"= ",
368
+ format.reset, val_str,
369
+ format.reset
370
+ )
371
+
372
+ values_printed += 1
373
+ value_ptr -= 1
374
+ except (debugger.Error, debugger.MemoryError):
375
+ break
376
+
377
+ if values_printed == 0:
378
+ self.terminal.print(format.dim, " (empty stack)", format.reset)
379
+
380
+ except (debugger.Error, RuntimeError) as e:
381
+ # Silently skip if we can't read stack values
382
+ pass
383
+
384
+ def _format_value_brief(self, val):
385
+ """Get a brief string representation of a VALUE.
386
+
387
+ Args:
388
+ val: Ruby VALUE
389
+
390
+ Returns:
391
+ Brief string description
392
+ """
393
+ try:
394
+ # Use value.py's interpret function to get the typed object
395
+ obj = rvalue.interpret(val)
396
+
397
+ # Get string representation
398
+ obj_str = str(obj)
399
+
400
+ # Truncate if too long
401
+ if len(obj_str) > 60:
402
+ return obj_str[:57] + "..."
403
+
404
+ return obj_str
405
+
406
+ except Exception as e:
407
+ return f"<error: {e}>"
408
+
409
+ def _get_lineno(self, cfp):
410
+ """Get line number for a control frame.
411
+
412
+ Args:
413
+ cfp: Control frame pointer
414
+
415
+ Returns:
416
+ Line number as int or "???" if unavailable
417
+ """
418
+ try:
419
+ iseq = cfp['iseq']
420
+ pc = cfp['pc']
421
+
422
+ if int(pc) == 0:
423
+ return "???"
424
+
425
+ iseq_body = iseq['body']
426
+ iseq_encoded = iseq_body['iseq_encoded']
427
+ iseq_size = int(iseq_body['iseq_size'])
428
+
429
+ pc_offset = int(pc - iseq_encoded)
430
+
431
+ if pc_offset < 0 or pc_offset >= iseq_size:
432
+ return "???"
433
+
434
+ # Try to get line info
435
+ insns_info = iseq_body['insns_info']
436
+ positions = insns_info['positions']
437
+
438
+ if int(positions) != 0:
439
+ position = positions[pc_offset]
440
+ lineno = int(position['lineno'])
441
+ if lineno >= 0:
442
+ return lineno
443
+
444
+ # Fall back to first_lineno
445
+ return int(iseq_body['location']['first_lineno'])
446
+
447
+ except:
448
+ return "???"
449
+
450
+ def _get_exception_class(self, exc_value):
451
+ """Get the class name of an exception object.
452
+
453
+ Delegates to rexception.RException for proper exception handling.
454
+
455
+ Args:
456
+ exc_value: Exception VALUE
457
+
458
+ Returns:
459
+ Class name as string
460
+ """
461
+ try:
462
+ exc = rexception.RException(exc_value)
463
+ return exc.class_name
464
+ except Exception:
465
+ # Fallback if RException can't be created
466
+ try:
467
+ rbasic = exc_value.cast(self._rbasic_type)
468
+ klass = rbasic['klass']
469
+ return f"Exception(klass=0x{int(klass):x})"
470
+ except:
471
+ raise
472
+
473
+ def _get_exception_message(self, exc_value):
474
+ """Get the message from an exception object.
475
+
476
+ Delegates to rexception.RException for proper exception handling.
477
+
478
+ Args:
479
+ exc_value: Exception VALUE
480
+
481
+ Returns:
482
+ Message string or None if unavailable
483
+ """
484
+ try:
485
+ exc = rexception.RException(exc_value)
486
+ return exc.message
487
+ except Exception:
488
+ # If RException can't be created, return None
489
+ return None
490
+
491
+ def _value_to_string(self, val):
492
+ """Convert a Ruby VALUE to a Python string.
493
+
494
+ Args:
495
+ val: Ruby VALUE
496
+
497
+ Returns:
498
+ String representation
499
+ """
500
+ try:
501
+ # Use the rvalue.interpret infrastructure for proper type handling
502
+ obj = rvalue.interpret(val)
503
+
504
+ # For strings, get the actual content
505
+ if hasattr(obj, 'to_str'):
506
+ return obj.to_str()
507
+
508
+ # For immediates and other types, convert to string
509
+ obj_str = str(obj)
510
+
511
+ # Strip the type tag if present (e.g., "<T_FIXNUM> 42" -> "42")
512
+ if obj_str.startswith('<'):
513
+ # Find the end of the type tag
514
+ end_tag = obj_str.find('>')
515
+ if end_tag != -1 and end_tag + 2 < len(obj_str):
516
+ # Return the part after the tag and space
517
+ return obj_str[end_tag + 2:]
518
+
519
+ return obj_str
520
+
521
+ except Exception as e:
522
+ return f"<error:{e}>"
523
+
524
+ def _extract_path_from_pathobj(self, pathobj):
525
+ """Extract file path from pathobj (can be string or array).
526
+
527
+ Args:
528
+ pathobj: Ruby VALUE (either T_STRING or T_ARRAY)
529
+
530
+ Returns:
531
+ File path as string
532
+ """
533
+ try:
534
+ # Interpret the pathobj to get its type
535
+ obj = rvalue.interpret(pathobj)
536
+
537
+ # If it's an array, get the first element (the path)
538
+ if hasattr(obj, 'length') and hasattr(obj, 'get_item'):
539
+ if obj.length() > 0:
540
+ path_value = obj.get_item(0)
541
+ return self._value_to_string(path_value)
542
+
543
+ # Otherwise, treat it as a string directly
544
+ return self._value_to_string(pathobj)
545
+
546
+ except Exception as e:
547
+ return f"<error:{e}>"
546
548
 
547
549
 
548
- class RubyStackTraceCommand(debugger.Command):
549
- """Print combined C and Ruby backtrace for current fiber or thread.
550
-
551
- Usage: rb-stack-trace [--values]
552
-
553
- Shows backtrace for:
554
- - Currently selected fiber (if rb-fiber-switch was used)
555
- - Current thread execution context (if no fiber selected)
556
-
557
- Options:
558
- --values Show all Ruby VALUEs on each frame's stack pointer
559
-
560
- The output shows both C frames and Ruby frames intermixed,
561
- giving a complete picture of the call stack.
562
- """
563
-
564
- def __init__(self):
565
- super(RubyStackTraceCommand, self).__init__("rb-stack-trace", debugger.COMMAND_USER)
566
- self.printer = RubyStackPrinter()
567
-
568
- def usage(self):
569
- """Print usage information."""
570
- print("Usage: rb-stack-trace [--values]")
571
- print("Examples:")
572
- print(" rb-stack-trace # Show backtrace for current fiber/thread")
573
- print(" rb-stack-trace --values # Show backtrace with stack VALUEs")
574
-
575
-
576
- def invoke(self, arg, from_tty):
577
- """Execute the stack trace command."""
578
- try:
579
- # Parse arguments
580
- import command
581
- arguments = command.parse_arguments(arg if arg else "")
582
- self.printer.show_values = arguments.has_flag('values')
583
-
584
- # Create terminal for formatting
585
- self.printer.terminal = format.create_terminal(from_tty)
586
-
587
- # Check if a fiber is currently selected
588
- # Import here to avoid circular dependency
589
- import fiber
590
- current_fiber = fiber.get_current_fiber()
591
-
592
- if current_fiber:
593
- # Use the selected fiber's execution context
594
- print(f"Stack trace for selected fiber:")
595
- print(f" Fiber: ", end='')
596
- print(self.printer.terminal.print_type_tag('T_DATA', int(current_fiber.value), None))
597
- print()
598
-
599
- ec = current_fiber.pointer['cont']['saved_ec'].address
600
- self.printer.print_backtrace(ec, from_tty)
601
- else:
602
- # Use current thread's execution context
603
- print("Stack trace for current thread:")
604
- print()
605
-
606
- try:
607
- ctx = context.RubyContext.current()
608
-
609
- if ctx is None:
610
- print("Error: No execution context available")
611
- print("Either select a fiber with 'rb-fiber-switch' or ensure Ruby is running")
612
- print("\nTroubleshooting:")
613
- print(" - Check if Ruby symbols are loaded")
614
- print(" - Ensure the process is stopped at a Ruby frame")
615
- return
616
-
617
- self.printer.print_backtrace(ctx.ec, from_tty)
618
- except debugger.Error as e:
619
- print(f"Error getting execution context: {e}")
620
- print("Try selecting a fiber first with 'rb-fiber-switch'")
621
- return
622
-
623
- except Exception as e:
624
- print(f"Error: {e}")
625
- import traceback
626
- traceback.print_exc()
550
+ class RubyStackTraceHandler:
551
+ """Print combined C and Ruby backtrace for current fiber or thread."""
552
+
553
+ USAGE = command.Usage(
554
+ summary="Print combined C and Ruby backtrace",
555
+ parameters=[],
556
+ options={},
557
+ flags=[('values', 'Show stack VALUEs in addition to backtrace')],
558
+ examples=[
559
+ ("rb-stack-trace", "Show backtrace for current fiber/thread"),
560
+ ("rb-stack-trace --values", "Show backtrace with stack VALUEs")
561
+ ]
562
+ )
563
+
564
+ def __init__(self):
565
+ self.printer = RubyStackPrinter()
566
+
567
+ def invoke(self, arguments, terminal):
568
+ """Execute the stack trace command."""
569
+ try:
570
+ # Get flags
571
+ self.printer.show_values = arguments.has_flag('values')
572
+
573
+ # Set terminal for formatting
574
+ self.printer.terminal = terminal
575
+
576
+ # Check if a fiber is currently selected
577
+ # Import here to avoid circular dependency
578
+ import fiber
579
+ current_fiber = fiber.get_current_fiber()
580
+
581
+ if current_fiber:
582
+ # Use the selected fiber's execution context
583
+ print(f"Stack trace for selected fiber:")
584
+ print(f" Fiber: ", end='')
585
+ self.printer.terminal.print_type_tag('T_DATA', int(current_fiber.value), None)
586
+ print()
587
+ print()
588
+
589
+ ec = current_fiber.pointer['cont']['saved_ec'].address
590
+ self.printer.print_backtrace(ec, True)
591
+ else:
592
+ # Use current thread's execution context
593
+ print("Stack trace for current thread:")
594
+ print()
595
+
596
+ try:
597
+ ctx = context.RubyContext.current()
598
+
599
+ if ctx is None:
600
+ print("Error: No execution context available")
601
+ print("Either select a fiber with 'rb-fiber-switch' or ensure Ruby is running")
602
+ print("\nTroubleshooting:")
603
+ print(" - Check if Ruby symbols are loaded")
604
+ print(" - Ensure the process is stopped at a Ruby frame")
605
+ return
606
+
607
+ self.printer.print_backtrace(ctx.ec, True)
608
+ except debugger.Error as e:
609
+ print(f"Error getting execution context: {e}")
610
+ print("Try selecting a fiber first with 'rb-fiber-switch'")
611
+ return
612
+
613
+ except Exception as e:
614
+ print(f"Error: {e}")
615
+ import traceback
616
+ traceback.print_exc()
627
617
 
628
618
 
629
619
  # Register commands
630
- RubyStackTraceCommand()
620
+ debugger.register("rb-stack-trace", RubyStackTraceHandler, usage=RubyStackTraceHandler.USAGE)