1use std::collections::{HashSet, VecDeque};
16
17use compiler::compiler::DebugInfo;
18use parser::lexer::token::Span;
19use serde::Serialize;
20
21use crate::report::summarize_gc_object;
22use crate::value::{format_hash_key_label, EdgeRelation, HashKey, Value, ValueCell, ValueKind};
23use crate::{Frame, GcHeap, GcId, GcRef};
24
25pub const MAX_DEBUGGER_HITS: usize = 25;
26pub const MAX_DEBUGGER_OBJECTS: usize = 100; pub const MAX_DEBUGGER_EDGES: usize = 250; pub const MAX_DEBUGGER_DISPLAY_CHARS: usize = 64;
29pub const MAX_DEBUGGER_SUMMARY_DEPTH: usize = 2;
30pub const MAX_DEBUGGER_MEMBERS: usize = 8; #[derive(Clone, Debug, Eq, PartialEq, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct DebuggerHit {
35 pub index: usize,
37 pub span: Option<Span>,
39 pub frames: Vec<FrameView>,
41 pub globals: Vec<SlotView>,
43 pub heap: HeapView,
44}
45
46#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct FrameView {
49 pub name: String,
50 pub current_span: Option<Span>,
51 pub callee: Option<ValueView>,
53 pub locals: Vec<SlotView>,
54 pub captures: Vec<CaptureView>,
55 pub temporaries: Vec<StackSlotView>,
59}
60
61#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
62#[serde(rename_all = "camelCase")]
63pub struct SlotView {
64 pub name: String,
65 pub slot: usize,
66 pub initialized: bool,
70 pub value: Option<ValueView>,
71}
72
73#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
74#[serde(rename_all = "camelCase")]
75pub struct CaptureView {
76 pub name: String,
77 pub index: usize,
78 pub value: ValueView,
79}
80
81#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
82#[serde(rename_all = "camelCase")]
83pub struct StackSlotView {
84 pub slot: usize,
86 pub value: ValueView,
87}
88
89#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
90#[serde(rename_all = "camelCase")]
91pub struct ValueView {
92 pub kind: ValueKind,
93 pub display: String,
94 pub heap_id: Option<usize>,
98}
99
100#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct HeapObjectView {
103 pub id: usize,
104 pub kind: ValueKind,
105 pub label: String,
106 pub members: Vec<HeapMemberView>,
108}
109
110#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
111#[serde(rename_all = "camelCase")]
112pub struct HeapMemberView {
113 pub relation: EdgeRelation,
114 pub display: String,
115}
116
117#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
118#[serde(rename_all = "camelCase")]
119pub struct HeapEdgeView {
120 pub from: usize,
121 pub to: usize,
122 pub relation: EdgeRelation,
123}
124
125#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
126#[serde(rename_all = "camelCase")]
127pub struct HeapView {
128 pub objects: Vec<HeapObjectView>,
129 pub edges: Vec<HeapEdgeView>,
130 pub omitted_objects: usize,
134 pub omitted_edges: usize,
137}
138
139pub(crate) struct HitContext<'a> {
143 pub heap: &'a GcHeap,
144 pub frames: &'a [Frame],
146 pub stack: &'a [GcRef],
147 pub sp: usize,
148 pub globals: &'a [GcRef],
149 pub global_bindings: &'a [compiler::compiler::BindingDebugInfo],
150 pub globals_initialized: &'a [bool],
151 pub main_debug_info: &'a DebugInfo,
152 pub function_debug_info: &'a std::collections::HashMap<GcRef, DebugInfo>,
153 pub index: usize,
155}
156
157pub(crate) fn collect_hit(ctx: HitContext) -> DebuggerHit {
158 let mut roots: Vec<GcRef> = Vec::new();
163
164 let mut globals = Vec::with_capacity(ctx.global_bindings.len());
165 for binding in ctx.global_bindings {
166 globals.push(slot_view(
167 ctx.heap,
168 &binding.name,
169 binding.slot,
170 ctx.globals_initialized
171 .get(binding.slot)
172 .copied()
173 .unwrap_or(false),
174 ctx.globals.get(binding.slot).copied(),
175 &mut roots,
176 ));
177 }
178
179 let mut frames = Vec::with_capacity(ctx.frames.len());
180 for (frame_number, frame) in ctx.frames.iter().enumerate() {
181 let is_main = frame_number == 0;
182 let is_current = frame_number == ctx.frames.len() - 1;
183 let debug_info = if is_main {
184 Some(ctx.main_debug_info)
185 } else {
186 ctx.function_debug_info.get(&frame.cl.func)
187 };
188
189 let callee = (!is_main)
190 .then(|| frame.base_pointer.checked_sub(1))
191 .flatten()
192 .and_then(|slot| ctx.stack.get(slot))
193 .map(|reference| rooted_value_view(ctx.heap, *reference, &mut roots));
194
195 let mut locals = Vec::new();
196 if let Some(debug_info) = debug_info {
197 for binding in &debug_info.local_bindings {
198 locals.push(slot_view(
199 ctx.heap,
200 &binding.name,
201 binding.slot,
202 frame
203 .initialized
204 .get(binding.slot)
205 .copied()
206 .unwrap_or(false),
207 ctx.stack.get(frame.base_pointer + binding.slot).copied(),
208 &mut roots,
209 ));
210 }
211 }
212
213 let free_names = debug_info
214 .map(|info| info.free_names.as_slice())
215 .unwrap_or(&[]);
216 let captures = frame
217 .cl
218 .free
219 .iter()
220 .enumerate()
221 .map(|(index, reference)| CaptureView {
222 name: free_names
223 .get(index)
224 .cloned()
225 .unwrap_or_else(|| format!("<free {}>", index)),
226 index,
227 value: rooted_value_view(ctx.heap, *reference, &mut roots),
228 })
229 .collect();
230
231 frames.push(FrameView {
232 name: frame_name(ctx.heap, frame, is_main),
233 current_span: (frame.ip >= 0)
234 .then_some(frame.ip as usize)
235 .and_then(|pc| debug_info.and_then(|info| info.span_for_pc(pc)))
236 .cloned(),
237 callee,
238 locals,
239 captures,
240 temporaries: Vec::new(),
243 });
244
245 if is_current {
246 let first_temporary = frame.base_pointer + frame.initialized.len();
247 let temporaries = (first_temporary..ctx.sp)
248 .filter_map(|slot| ctx.stack.get(slot).map(|reference| (slot, *reference)))
249 .map(|(slot, reference)| StackSlotView {
250 slot,
251 value: rooted_value_view(ctx.heap, reference, &mut roots),
252 })
253 .collect();
254 frames
255 .last_mut()
256 .expect("current frame was just pushed")
257 .temporaries = temporaries;
258 }
259 }
260
261 let span = frames.last().and_then(|frame| frame.current_span.clone());
262 let heap = project_heap(ctx.heap, &roots);
263
264 DebuggerHit {
265 index: ctx.index,
266 span,
267 frames,
268 globals,
269 heap,
270 }
271}
272
273fn slot_view(
274 heap: &GcHeap,
275 name: &str,
276 slot: usize,
277 initialized: bool,
278 reference: Option<GcRef>,
279 roots: &mut Vec<GcRef>,
280) -> SlotView {
281 let value = match reference {
282 Some(reference) if initialized => Some(rooted_value_view(heap, reference, roots)),
283 _ => None,
284 };
285 SlotView {
286 name: name.to_string(),
287 slot,
288 initialized: initialized && reference.is_some(),
291 value,
292 }
293}
294
295fn frame_name(heap: &GcHeap, frame: &Frame, is_main: bool) -> String {
296 if is_main {
297 return "main".to_string();
298 }
299 match try_value(heap, frame.cl.func) {
300 Some(Value::CompiledFunction(function)) if !function.name.is_empty() => {
301 function.name.clone()
302 }
303 _ => "<anonymous>".to_string(),
304 }
305}
306
307fn rooted_value_view(heap: &GcHeap, reference: GcRef, roots: &mut Vec<GcRef>) -> ValueView {
308 let view = value_view(heap, reference);
309 if view.heap_id.is_some() {
310 roots.push(reference);
311 }
312 view
313}
314
315pub(crate) fn value_view(heap: &GcHeap, reference: GcRef) -> ValueView {
316 let kind = try_value(heap, reference)
317 .map(Value::kind)
318 .unwrap_or(ValueKind::Other);
319 ValueView {
320 kind,
321 display: bounded_display(heap, reference),
322 heap_id: is_heap_node(kind).then_some(reference.0),
323 }
324}
325
326fn is_heap_node(kind: ValueKind) -> bool {
328 matches!(
329 kind,
330 ValueKind::String
331 | ValueKind::Array
332 | ValueKind::Hash
333 | ValueKind::Closure
334 | ValueKind::Class
335 | ValueKind::Instance
336 | ValueKind::BoundMethod
337 | ValueKind::Error
338 )
339}
340
341fn is_inline_scalar(kind: ValueKind) -> bool {
343 matches!(kind, ValueKind::Integer | ValueKind::Boolean | ValueKind::Null | ValueKind::Builtin)
344}
345
346fn try_value(heap: &GcHeap, reference: GcRef) -> Option<&Value> {
347 heap.runtime()
348 .object_downcast::<ValueCell>(reference.0)
349 .map(|cell| &cell.value)
350}
351
352struct BoundedText {
356 out: String,
357 remaining: usize,
358 truncated: bool,
359}
360
361impl BoundedText {
362 fn new(limit: usize) -> Self {
363 BoundedText {
364 out: String::new(),
365 remaining: limit,
366 truncated: false,
367 }
368 }
369
370 fn push(&mut self, text: &str) {
371 if self.truncated {
372 return;
373 }
374 for ch in text.chars() {
375 if self.remaining == 0 {
376 self.truncated = true;
377 return;
378 }
379 self.out.push(ch);
380 self.remaining -= 1;
381 }
382 }
383
384 fn finish(mut self) -> String {
385 if self.truncated {
386 self.out.pop();
388 self.out.push('…');
389 }
390 self.out
391 }
392}
393
394fn bounded_display(heap: &GcHeap, reference: GcRef) -> String {
395 let mut text = BoundedText::new(MAX_DEBUGGER_DISPLAY_CHARS);
396 let mut visiting = HashSet::new();
397 append_reference(heap, reference, MAX_DEBUGGER_SUMMARY_DEPTH, &mut visiting, &mut text);
398 text.finish()
399}
400
401fn append_reference(
402 heap: &GcHeap,
403 reference: GcRef,
404 depth: usize,
405 visiting: &mut HashSet<GcId>,
406 text: &mut BoundedText,
407) {
408 if text.truncated {
409 return;
410 }
411 let Some(value) = try_value(heap, reference) else {
412 text.push("<invalid>");
413 return;
414 };
415 if !visiting.insert(reference.0) {
416 text.push(&format!("[cycle #{}]", reference.0));
417 return;
418 }
419 append_value(heap, value, depth, visiting, text);
420 visiting.remove(&reference.0);
421}
422
423fn append_value(
424 heap: &GcHeap,
425 value: &Value,
426 depth: usize,
427 visiting: &mut HashSet<GcId>,
428 text: &mut BoundedText,
429) {
430 match value {
431 Value::Integer(value) => text.push(&value.to_string()),
432 Value::Boolean(value) => text.push(&value.to_string()),
433 Value::String(value) => text.push(value),
434 Value::Null => text.push("null"),
435 Value::Error(message) => text.push(message),
436 Value::Builtin(_) => text.push("[builtin function]"),
437 Value::CompiledFunction(_) => text.push("[compiled function]"),
438 Value::Closure(_) => text.push("[closure function]"),
439 Value::Class(class) => {
440 text.push("[class ");
441 text.push(&class.name);
442 text.push("]");
443 }
444 Value::Instance(instance) => {
445 text.push("[object ");
446 text.push(&referenced_class_name(heap, instance.class));
447 text.push("]");
448 }
449 Value::BoundMethod(method) => {
450 text.push("[bound method ");
451 text.push(&receiver_class_name(heap, method.receiver));
452 text.push(".");
453 text.push(&method.name);
454 text.push("]");
455 }
456 Value::Array(items) => {
457 if depth == 0 {
458 text.push("[…]");
459 return;
460 }
461 text.push("[");
462 for (position, item) in items.iter().take(MAX_DEBUGGER_MEMBERS).enumerate() {
463 if position > 0 {
464 text.push(", ");
465 }
466 append_reference(heap, *item, depth - 1, visiting, text);
467 }
468 if items.len() > MAX_DEBUGGER_MEMBERS {
469 text.push(", …");
470 }
471 text.push("]");
472 }
473 Value::Hash(map) => {
474 if depth == 0 {
475 text.push("{…}");
476 return;
477 }
478 let mut entries: Vec<(&HashKey, &GcRef)> = map.iter().collect();
479 entries.sort_by_key(|(key, _)| *key);
480 text.push("{");
481 for (position, (key, value)) in entries.iter().take(MAX_DEBUGGER_MEMBERS).enumerate() {
482 if position > 0 {
483 text.push(", ");
484 }
485 text.push(&format_hash_key_label(key));
486 text.push(": ");
487 append_reference(heap, **value, depth - 1, visiting, text);
488 }
489 if entries.len() > MAX_DEBUGGER_MEMBERS {
490 text.push(", …");
491 }
492 text.push("}");
493 }
494 }
495}
496
497fn referenced_class_name(heap: &GcHeap, class: GcRef) -> String {
498 match try_value(heap, class) {
499 Some(Value::Class(class)) => class.name.clone(),
500 _ => "<invalid class>".to_string(),
501 }
502}
503
504fn receiver_class_name(heap: &GcHeap, receiver: GcRef) -> String {
505 match try_value(heap, receiver) {
506 Some(Value::Instance(instance)) => referenced_class_name(heap, instance.class),
507 _ => "<invalid receiver>".to_string(),
508 }
509}
510
511fn project_heap(heap: &GcHeap, roots: &[GcRef]) -> HeapView {
515 let kinds = heap.value_kinds_by_id();
516 let node_ids: Vec<GcId> = kinds
517 .iter()
518 .filter(|(_, kind)| is_heap_node(**kind))
519 .map(|(id, _)| *id)
520 .collect();
521 let node_id_set: HashSet<GcId> = node_ids.iter().copied().collect();
522
523 let mut selected: Vec<GcId> = Vec::new();
524 let mut selected_set: HashSet<GcId> = HashSet::new();
525 let mut queue: VecDeque<GcId> = VecDeque::new();
526 let try_select = |id: GcId,
527 selected: &mut Vec<GcId>,
528 selected_set: &mut HashSet<GcId>,
529 queue: &mut VecDeque<GcId>| {
530 if selected.len() >= MAX_DEBUGGER_OBJECTS
531 || !node_id_set.contains(&id)
532 || !selected_set.insert(id)
533 {
534 return;
535 }
536 selected.push(id);
537 queue.push_back(id);
538 };
539
540 for root in roots {
541 try_select(root.0, &mut selected, &mut selected_set, &mut queue);
542 }
543 while let Some(id) = queue.pop_front() {
544 if let Some(cell) = heap.runtime().object_downcast::<ValueCell>(id) {
545 let mut targets = Vec::new();
546 cell.value.visit_edges(|_, target| targets.push(target));
547 for target in targets {
548 try_select(target.0, &mut selected, &mut selected_set, &mut queue);
549 }
550 }
551 }
552 for id in &node_ids {
553 try_select(*id, &mut selected, &mut selected_set, &mut queue);
554 }
555
556 let mut objects = Vec::with_capacity(selected.len());
557 let mut edges = Vec::new();
558 let mut omitted_edges = 0usize;
559 for &id in &selected {
560 let summary = summarize_gc_object(heap.runtime(), id);
561 let mut members = Vec::new();
562 if let Some(cell) = heap.runtime().object_downcast::<ValueCell>(id) {
563 cell.value.visit_edges(|relation, target| {
564 let target_kind = kinds.get(&target.0).copied().unwrap_or(ValueKind::Other);
565 if is_inline_scalar(target_kind) {
566 if members.len() < MAX_DEBUGGER_MEMBERS {
567 members.push(HeapMemberView {
568 relation,
569 display: bounded_display(heap, target),
570 });
571 }
572 } else if is_heap_node(target_kind) {
573 if selected_set.contains(&target.0) && edges.len() < MAX_DEBUGGER_EDGES {
574 edges.push(HeapEdgeView {
575 from: id,
576 to: target.0,
577 relation,
578 });
579 } else {
580 omitted_edges += 1;
581 }
582 }
583 });
586 }
587 objects.push(HeapObjectView {
588 id,
589 kind: summary.kind,
590 label: summary
594 .label
595 .strip_suffix(&format!("#{}", id))
596 .unwrap_or(&summary.label)
597 .to_string(),
598 members,
599 });
600 }
601
602 HeapView {
603 objects,
604 edges,
605 omitted_objects: node_ids.len() - selected.len(),
606 omitted_edges,
607 }
608}