1use crate::error_handling::{AutogradError, AutogradResult};
19use crate::gradient_tracer::{EventType, PathId, TraceEvent, TraceEventId};
20use parking_lot::{Mutex, RwLock};
21use serde::{Deserialize, Serialize};
22use std::cmp::Ordering;
23use std::collections::{HashMap, VecDeque};
24use std::sync::Arc;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum DebuggerState {
29 Inactive,
31 Paused,
33 Running,
35 Stepping,
37 Continuing,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub enum BreakpointCondition {
44 OperationName(String),
46
47 TensorId(String),
49
50 Anomaly,
52
53 MemoryThreshold(usize),
55
56 OperationCount(usize),
58
59 GradientExplosion(f64),
61
62 GradientVanishing(f64),
64
65 Custom(String),
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Breakpoint {
72 pub id: u64,
74
75 pub condition: BreakpointCondition,
77
78 pub enabled: bool,
80
81 pub hit_count: usize,
83
84 pub description: String,
86}
87
88impl Breakpoint {
89 pub fn new(id: u64, condition: BreakpointCondition, description: String) -> Self {
91 Self {
92 id,
93 condition,
94 enabled: true,
95 hit_count: 0,
96 description,
97 }
98 }
99
100 pub fn should_trigger(&mut self, event: &TraceEvent, context: &DebugContext) -> bool {
102 if !self.enabled {
103 return false;
104 }
105
106 let triggered = match &self.condition {
107 BreakpointCondition::OperationName(name) => &event.operation == name,
108 BreakpointCondition::TensorId(id) => {
109 event.input_ids.contains(id) || event.output_ids.contains(id)
110 }
111 BreakpointCondition::Anomaly => {
112 matches!(event.event_type, EventType::Custom)
113 }
114 BreakpointCondition::MemoryThreshold(threshold) => {
115 event.memory_allocated.unwrap_or(0) > *threshold
116 }
117 BreakpointCondition::OperationCount(count) => self.hit_count >= *count,
118 BreakpointCondition::GradientExplosion(threshold) => {
119 extract_gradient_norm(event, context).is_some_and(|norm| norm > *threshold)
123 }
124 BreakpointCondition::GradientVanishing(threshold) => {
125 extract_gradient_norm(event, context).is_some_and(|norm| norm < *threshold)
128 }
129 BreakpointCondition::Custom(expr) => {
130 match evaluate_custom_expression(expr, event, context) {
134 Ok(result) => result,
135 Err(err) => {
136 tracing::warn!("custom breakpoint expression error: {}", err);
137 false
138 }
139 }
140 }
141 };
142
143 if triggered {
144 self.hit_count += 1;
145 }
146
147 triggered
148 }
149}
150
151fn extract_gradient_norm(event: &TraceEvent, context: &DebugContext) -> Option<f64> {
175 if let Some(raw) = event.metadata.get("gradient_norm") {
177 if let Ok(value) = raw.trim().parse::<f64>() {
178 if value.is_finite() {
179 return Some(value.abs());
180 }
181 }
182 }
183
184 if let Some(raw) = event.metadata.get("gradient_values") {
186 if let Some(norm) = l2_norm_from_csv(raw) {
187 return Some(norm);
188 }
189 }
190
191 let is_gradient_event = matches!(
194 event.event_type,
195 EventType::GradientComputation | EventType::BackwardBegin | EventType::BackwardEnd
196 );
197 if !is_gradient_event {
198 return None;
199 }
200
201 let mut sum_sq = 0.0_f64;
202 let mut found = false;
203 for output_id in &event.output_ids {
204 if let Some(descriptor) = context.gradient_values.get(output_id) {
205 if let Some(tensor_norm) = parse_gradient_descriptor(descriptor) {
206 sum_sq += tensor_norm * tensor_norm;
207 found = true;
208 }
209 }
210 }
211
212 if found {
213 Some(sum_sq.sqrt())
214 } else {
215 None
216 }
217}
218
219fn l2_norm_from_csv(raw: &str) -> Option<f64> {
225 let mut sum_sq = 0.0_f64;
226 let mut count = 0_usize;
227 for token in raw.split(',') {
228 let token = token.trim();
229 if token.is_empty() {
230 continue;
231 }
232 let value: f64 = token.parse().ok()?;
233 if !value.is_finite() {
234 return None;
235 }
236 sum_sq += value * value;
237 count += 1;
238 }
239 if count == 0 {
240 None
241 } else {
242 Some(sum_sq.sqrt())
243 }
244}
245
246fn parse_gradient_descriptor(descriptor: &str) -> Option<f64> {
251 let trimmed = descriptor.trim();
252 if let Some(rest) = trimmed.strip_prefix("norm=") {
253 let value: f64 = rest.trim().parse().ok()?;
254 return if value.is_finite() {
255 Some(value.abs())
256 } else {
257 None
258 };
259 }
260 l2_norm_from_csv(trimmed)
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269enum CompareOp {
270 Eq,
271 Ne,
272 Lt,
273 Le,
274 Gt,
275 Ge,
276}
277
278#[derive(Debug, Clone, PartialEq)]
280enum Token {
281 Ident(String),
283 Number(f64),
285 Str(String),
287 Compare(CompareOp),
289 And,
291 Or,
293}
294
295enum FieldValue {
297 Number(f64),
298 Text(String),
299 Missing,
302}
303
304enum Operand {
306 Number(f64),
307 Text(String),
308}
309
310pub fn evaluate_custom_expression(
347 expr: &str,
348 event: &TraceEvent,
349 context: &DebugContext,
350) -> AutogradResult<bool> {
351 let tokens = tokenize_expression(expr)?;
352 if tokens.is_empty() {
353 return Err(expression_error(expr, "expression is empty"));
354 }
355
356 let mut parser = ExpressionParser {
357 tokens: &tokens,
358 pos: 0,
359 expr,
360 event,
361 context,
362 };
363 let result = parser.parse_or()?;
364 if parser.pos != tokens.len() {
365 return Err(expression_error(
366 expr,
367 "unexpected trailing tokens after expression",
368 ));
369 }
370 Ok(result)
371}
372
373fn expression_error(expr: &str, reason: &str) -> AutogradError {
375 AutogradError::Configuration {
376 parameter: "custom_breakpoint_expression".to_string(),
377 value: expr.to_string(),
378 reason: reason.to_string(),
379 valid_range: Some(
380 "grammar: <field> <op> <literal> [(&& | ||) <field> <op> <literal>]*; \
381 ops: == != < <= > >=; \
382 fields: operation, event_type, memory, memory_allocated, \
383 memory_deallocated, memory_usage, operation_index, total_operations, \
384 input_count, output_count, gradient_norm, duration_micros"
385 .to_string(),
386 ),
387 }
388}
389
390fn tokenize_expression(expr: &str) -> AutogradResult<Vec<Token>> {
392 let chars: Vec<char> = expr.chars().collect();
393 let mut tokens = Vec::new();
394 let mut i = 0;
395
396 while i < chars.len() {
397 let c = chars[i];
398 if c.is_whitespace() {
399 i += 1;
400 continue;
401 }
402
403 match c {
404 '"' => {
405 let mut literal = String::new();
406 i += 1;
407 let mut closed = false;
408 while i < chars.len() {
409 if chars[i] == '"' {
410 closed = true;
411 i += 1;
412 break;
413 }
414 literal.push(chars[i]);
415 i += 1;
416 }
417 if !closed {
418 return Err(expression_error(expr, "unterminated string literal"));
419 }
420 tokens.push(Token::Str(literal));
421 }
422 '=' => {
423 if chars.get(i + 1) == Some(&'=') {
424 tokens.push(Token::Compare(CompareOp::Eq));
425 i += 2;
426 } else {
427 return Err(expression_error(
428 expr,
429 "expected '==' (a single '=' is not a valid operator)",
430 ));
431 }
432 }
433 '!' => {
434 if chars.get(i + 1) == Some(&'=') {
435 tokens.push(Token::Compare(CompareOp::Ne));
436 i += 2;
437 } else {
438 return Err(expression_error(expr, "expected '!='"));
439 }
440 }
441 '<' => {
442 if chars.get(i + 1) == Some(&'=') {
443 tokens.push(Token::Compare(CompareOp::Le));
444 i += 2;
445 } else {
446 tokens.push(Token::Compare(CompareOp::Lt));
447 i += 1;
448 }
449 }
450 '>' => {
451 if chars.get(i + 1) == Some(&'=') {
452 tokens.push(Token::Compare(CompareOp::Ge));
453 i += 2;
454 } else {
455 tokens.push(Token::Compare(CompareOp::Gt));
456 i += 1;
457 }
458 }
459 '&' => {
460 if chars.get(i + 1) == Some(&'&') {
461 tokens.push(Token::And);
462 i += 2;
463 } else {
464 return Err(expression_error(expr, "expected '&&'"));
465 }
466 }
467 '|' => {
468 if chars.get(i + 1) == Some(&'|') {
469 tokens.push(Token::Or);
470 i += 2;
471 } else {
472 return Err(expression_error(expr, "expected '||'"));
473 }
474 }
475 _ if c.is_ascii_digit()
476 || c == '.'
477 || (c == '-'
478 && chars
479 .get(i + 1)
480 .is_some_and(|n| n.is_ascii_digit() || *n == '.')) =>
481 {
482 let start = i;
483 if chars[i] == '-' {
484 i += 1;
485 }
486 while i < chars.len() {
487 let ch = chars[i];
488 let is_exponent_sign =
489 (ch == '+' || ch == '-') && matches!(chars.get(i - 1), Some('e' | 'E'));
490 if ch.is_ascii_digit()
491 || ch == '.'
492 || ch == 'e'
493 || ch == 'E'
494 || is_exponent_sign
495 {
496 i += 1;
497 } else {
498 break;
499 }
500 }
501 let lexeme: String = chars[start..i].iter().collect();
502 let value: f64 = lexeme
503 .parse()
504 .map_err(|_| expression_error(expr, &format!("invalid number '{lexeme}'")))?;
505 tokens.push(Token::Number(value));
506 }
507 _ if c.is_alphabetic() || c == '_' => {
508 let start = i;
509 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
510 i += 1;
511 }
512 let ident: String = chars[start..i].iter().collect();
513 tokens.push(Token::Ident(ident));
514 }
515 _ => {
516 return Err(expression_error(
517 expr,
518 &format!("unexpected character '{c}'"),
519 ));
520 }
521 }
522 }
523
524 Ok(tokens)
525}
526
527struct ExpressionParser<'a> {
529 tokens: &'a [Token],
530 pos: usize,
531 expr: &'a str,
532 event: &'a TraceEvent,
533 context: &'a DebugContext,
534}
535
536impl ExpressionParser<'_> {
537 fn parse_or(&mut self) -> AutogradResult<bool> {
539 let mut result = self.parse_and()?;
540 while matches!(self.tokens.get(self.pos), Some(Token::Or)) {
541 self.pos += 1;
542 let rhs = self.parse_and()?;
545 result = result || rhs;
546 }
547 Ok(result)
548 }
549
550 fn parse_and(&mut self) -> AutogradResult<bool> {
552 let mut result = self.parse_comparison()?;
553 while matches!(self.tokens.get(self.pos), Some(Token::And)) {
554 self.pos += 1;
555 let rhs = self.parse_comparison()?;
556 result = result && rhs;
557 }
558 Ok(result)
559 }
560
561 fn parse_comparison(&mut self) -> AutogradResult<bool> {
563 let field_name = match self.tokens.get(self.pos) {
564 Some(Token::Ident(name)) => name.clone(),
565 Some(other) => {
566 return Err(expression_error(
567 self.expr,
568 &format!("expected a field name, found {other:?}"),
569 ));
570 }
571 None => {
572 return Err(expression_error(self.expr, "expected a field name"));
573 }
574 };
575 self.pos += 1;
576
577 let op = match self.tokens.get(self.pos) {
578 Some(Token::Compare(op)) => *op,
579 _ => {
580 return Err(expression_error(
581 self.expr,
582 &format!("expected a comparison operator after field '{field_name}'"),
583 ));
584 }
585 };
586 self.pos += 1;
587
588 let operand = match self.tokens.get(self.pos) {
589 Some(Token::Number(value)) => Operand::Number(*value),
590 Some(Token::Str(text)) => Operand::Text(text.clone()),
591 Some(Token::Ident(text)) => Operand::Text(text.clone()),
592 _ => {
593 return Err(expression_error(
594 self.expr,
595 &format!("expected a literal value after operator for field '{field_name}'"),
596 ));
597 }
598 };
599 self.pos += 1;
600
601 let field_value = self.resolve_field(&field_name)?;
602 self.compare(field_value, op, operand)
603 }
604
605 fn resolve_field(&self, name: &str) -> AutogradResult<FieldValue> {
607 let value = match name {
608 "operation" => FieldValue::Text(self.event.operation.clone()),
609 "event_type" => FieldValue::Text(format!("{:?}", self.event.event_type)),
610 "memory" | "memory_allocated" => {
611 FieldValue::Number(self.event.memory_allocated.unwrap_or(0) as f64)
612 }
613 "memory_deallocated" => {
614 FieldValue::Number(self.event.memory_deallocated.unwrap_or(0) as f64)
615 }
616 "memory_usage" => FieldValue::Number(self.context.memory_usage as f64),
617 "operation_index" => FieldValue::Number(self.context.operation_index as f64),
618 "total_operations" => FieldValue::Number(self.context.total_operations as f64),
619 "input_count" => FieldValue::Number(self.event.input_ids.len() as f64),
620 "output_count" => FieldValue::Number(self.event.output_ids.len() as f64),
621 "gradient_norm" => match extract_gradient_norm(self.event, self.context) {
622 Some(norm) => FieldValue::Number(norm),
623 None => FieldValue::Missing,
624 },
625 "duration_micros" => match self.event.duration {
626 Some(duration) => FieldValue::Number(duration.as_micros() as f64),
627 None => FieldValue::Missing,
628 },
629 other => {
630 return Err(expression_error(
631 self.expr,
632 &format!("unknown field '{other}'"),
633 ));
634 }
635 };
636 Ok(value)
637 }
638
639 fn compare(&self, field: FieldValue, op: CompareOp, operand: Operand) -> AutogradResult<bool> {
641 match field {
642 FieldValue::Missing => Ok(false),
645 FieldValue::Number(lhs) => {
646 let rhs = match operand {
647 Operand::Number(value) => value,
648 Operand::Text(text) => text.trim().parse::<f64>().map_err(|_| {
649 expression_error(
650 self.expr,
651 &format!(
652 "type mismatch: numeric field compared with non-numeric value '{text}'"
653 ),
654 )
655 })?,
656 };
657 Ok(apply_numeric_compare(lhs, op, rhs))
658 }
659 FieldValue::Text(lhs) => match op {
660 CompareOp::Eq | CompareOp::Ne => {
661 let rhs = match operand {
662 Operand::Text(text) => text,
663 Operand::Number(_) => {
664 return Err(expression_error(
665 self.expr,
666 "type mismatch: text field requires a string operand",
667 ));
668 }
669 };
670 Ok(if op == CompareOp::Eq {
671 lhs == rhs
672 } else {
673 lhs != rhs
674 })
675 }
676 _ => Err(expression_error(
677 self.expr,
678 "ordering comparison is not supported for text fields (use == or !=)",
679 )),
680 },
681 }
682 }
683}
684
685fn apply_numeric_compare(lhs: f64, op: CompareOp, rhs: f64) -> bool {
688 match lhs.partial_cmp(&rhs) {
689 None => false,
690 Some(ordering) => match op {
691 CompareOp::Eq => ordering == Ordering::Equal,
692 CompareOp::Ne => ordering != Ordering::Equal,
693 CompareOp::Lt => ordering == Ordering::Less,
694 CompareOp::Le => ordering != Ordering::Greater,
695 CompareOp::Gt => ordering == Ordering::Greater,
696 CompareOp::Ge => ordering != Ordering::Less,
697 },
698 }
699}
700
701#[derive(Debug, Clone)]
710struct HistoryNode {
711 id: TraceEventId,
712 parent_id: Option<TraceEventId>,
713 operation: String,
714}
715
716fn is_descendant(
719 node: TraceEventId,
720 ancestor: TraceEventId,
721 parent_map: &HashMap<TraceEventId, Option<TraceEventId>>,
722) -> bool {
723 let mut current = node;
724 let mut guard = 0_usize;
725 while let Some(Some(parent)) = parent_map.get(¤t) {
726 if *parent == ancestor {
727 return true;
728 }
729 current = *parent;
730 guard += 1;
731 if guard > parent_map.len() {
733 break;
734 }
735 }
736 false
737}
738
739fn build_call_stack(nodes: &[HistoryNode], index: usize) -> Vec<String> {
742 let by_id: HashMap<TraceEventId, &HistoryNode> = nodes.iter().map(|n| (n.id, n)).collect();
743 let mut stack = Vec::new();
744 let mut current = Some(nodes[index].id);
745 let mut guard = 0_usize;
746 while let Some(id) = current {
747 match by_id.get(&id) {
748 Some(node) => {
749 stack.push(node.operation.clone());
750 current = node.parent_id;
751 }
752 None => break,
753 }
754 guard += 1;
755 if guard > nodes.len() {
756 break;
757 }
758 }
759 stack.reverse();
760 stack
761}
762
763#[derive(Debug, Clone, Serialize, Deserialize)]
765pub struct Watchpoint {
766 pub id: u64,
768
769 pub tensor_id: String,
771
772 pub break_on_read: bool,
774
775 pub break_on_write: bool,
777
778 pub break_on_gradient: bool,
780
781 pub trigger_count: usize,
783}
784
785#[derive(Debug, Clone)]
787pub struct DebugContext {
788 pub operation_index: usize,
790
791 pub total_operations: usize,
793
794 pub call_stack: Vec<String>,
796
797 pub memory_usage: usize,
799
800 pub tensor_values: HashMap<String, String>,
802
803 pub gradient_values: HashMap<String, String>,
805}
806
807impl DebugContext {
808 pub fn new() -> Self {
810 Self {
811 operation_index: 0,
812 total_operations: 0,
813 call_stack: Vec::new(),
814 memory_usage: 0,
815 tensor_values: HashMap::new(),
816 gradient_values: HashMap::new(),
817 }
818 }
819}
820
821impl Default for DebugContext {
822 fn default() -> Self {
823 Self::new()
824 }
825}
826
827pub struct InteractiveDebugger {
829 state: Arc<RwLock<DebuggerState>>,
831
832 breakpoints: Arc<Mutex<HashMap<u64, Breakpoint>>>,
834
835 watchpoints: Arc<Mutex<HashMap<u64, Watchpoint>>>,
837
838 context: Arc<Mutex<DebugContext>>,
840
841 history: Arc<Mutex<VecDeque<TraceEvent>>>,
843
844 next_breakpoint_id: Arc<Mutex<u64>>,
846
847 next_watchpoint_id: Arc<Mutex<u64>>,
849
850 current_path: Arc<Mutex<Option<PathId>>>,
852
853 max_history_size: usize,
855
856 #[allow(dead_code)]
858 command_queue: Arc<Mutex<VecDeque<DebugCommand>>>,
859}
860
861#[derive(Debug, Clone, Serialize, Deserialize)]
863pub enum DebugCommand {
864 Step,
866
867 Continue,
869
870 StepOver,
872
873 StepOut,
875
876 Run,
878
879 Pause,
881
882 Restart,
884
885 InspectTensor(String),
887
888 InspectGradient(String),
890
891 ShowCallStack,
893
894 ShowMemory,
896
897 ListBreakpoints,
899
900 ListWatchpoints,
902}
903
904impl InteractiveDebugger {
905 pub fn new() -> Self {
907 Self {
908 state: Arc::new(RwLock::new(DebuggerState::Inactive)),
909 breakpoints: Arc::new(Mutex::new(HashMap::new())),
910 watchpoints: Arc::new(Mutex::new(HashMap::new())),
911 context: Arc::new(Mutex::new(DebugContext::new())),
912 history: Arc::new(Mutex::new(VecDeque::new())),
913 next_breakpoint_id: Arc::new(Mutex::new(1)), next_watchpoint_id: Arc::new(Mutex::new(1)), current_path: Arc::new(Mutex::new(None)),
916 max_history_size: 1000,
917 command_queue: Arc::new(Mutex::new(VecDeque::new())),
918 }
919 }
920
921 pub fn start_debugging(&self, path_id: PathId) -> AutogradResult<()> {
923 *self.state.write() = DebuggerState::Paused;
924 *self.current_path.lock() = Some(path_id);
925 self.context.lock().operation_index = 0;
926
927 Ok(())
928 }
929
930 pub fn stop_debugging(&self) {
932 *self.state.write() = DebuggerState::Inactive;
933 *self.current_path.lock() = None;
934 self.history.lock().clear();
935 }
936
937 pub fn state(&self) -> DebuggerState {
939 *self.state.read()
940 }
941
942 pub fn add_breakpoint(&self, condition: BreakpointCondition, description: String) -> u64 {
944 let id = {
945 let mut next_id = self.next_breakpoint_id.lock();
946 let id = *next_id;
947 *next_id += 1;
948 id
949 };
950
951 let breakpoint = Breakpoint::new(id, condition, description);
952 self.breakpoints.lock().insert(id, breakpoint);
953
954 id
955 }
956
957 pub fn remove_breakpoint(&self, id: u64) -> AutogradResult<()> {
959 self.breakpoints.lock().remove(&id);
960 Ok(())
961 }
962
963 pub fn enable_breakpoint(&self, id: u64) -> AutogradResult<()> {
965 if let Some(bp) = self.breakpoints.lock().get_mut(&id) {
966 bp.enabled = true;
967 Ok(())
968 } else {
969 Err(AutogradError::Configuration {
970 parameter: "breakpoint_id".to_string(),
971 value: id.to_string(),
972 reason: "Breakpoint not found".to_string(),
973 valid_range: None,
974 })
975 }
976 }
977
978 pub fn disable_breakpoint(&self, id: u64) -> AutogradResult<()> {
980 if let Some(bp) = self.breakpoints.lock().get_mut(&id) {
981 bp.enabled = false;
982 Ok(())
983 } else {
984 Err(AutogradError::Configuration {
985 parameter: "breakpoint_id".to_string(),
986 value: id.to_string(),
987 reason: "Breakpoint not found".to_string(),
988 valid_range: None,
989 })
990 }
991 }
992
993 pub fn list_breakpoints(&self) -> Vec<Breakpoint> {
995 self.breakpoints.lock().values().cloned().collect()
996 }
997
998 pub fn add_watchpoint(&self, tensor_id: String) -> u64 {
1000 let id = {
1001 let mut next_id = self.next_watchpoint_id.lock();
1002 let id = *next_id;
1003 *next_id += 1;
1004 id
1005 };
1006
1007 let watchpoint = Watchpoint {
1008 id,
1009 tensor_id,
1010 break_on_read: true,
1011 break_on_write: true,
1012 break_on_gradient: true,
1013 trigger_count: 0,
1014 };
1015
1016 self.watchpoints.lock().insert(id, watchpoint);
1017
1018 id
1019 }
1020
1021 pub fn remove_watchpoint(&self, id: u64) -> AutogradResult<()> {
1023 self.watchpoints.lock().remove(&id);
1024 Ok(())
1025 }
1026
1027 pub fn list_watchpoints(&self) -> Vec<Watchpoint> {
1029 self.watchpoints.lock().values().cloned().collect()
1030 }
1031
1032 pub fn process_event(&self, event: &TraceEvent) -> AutogradResult<bool> {
1034 {
1036 let mut history = self.history.lock();
1037 history.push_back(event.clone());
1038
1039 while history.len() > self.max_history_size {
1040 history.pop_front();
1041 }
1042 }
1043
1044 {
1046 let mut context = self.context.lock();
1047 context.operation_index += 1;
1048
1049 if let Some(mem) = event.memory_allocated {
1050 context.memory_usage += mem;
1051 }
1052
1053 if let Some(mem) = event.memory_deallocated {
1054 context.memory_usage = context.memory_usage.saturating_sub(mem);
1055 }
1056 }
1057
1058 let should_break = {
1060 let mut breakpoints = self.breakpoints.lock();
1061 let context = self.context.lock();
1062
1063 breakpoints
1064 .values_mut()
1065 .any(|bp| bp.should_trigger(event, &context))
1066 };
1067
1068 if should_break {
1069 *self.state.write() = DebuggerState::Paused;
1070 return Ok(true);
1071 }
1072
1073 match *self.state.read() {
1075 DebuggerState::Stepping => {
1076 *self.state.write() = DebuggerState::Paused;
1077 Ok(true)
1078 }
1079 DebuggerState::Paused => Ok(true),
1080 _ => Ok(false),
1081 }
1082 }
1083
1084 pub fn execute_command(&self, command: DebugCommand) -> AutogradResult<String> {
1086 match command {
1087 DebugCommand::Step => {
1088 *self.state.write() = DebuggerState::Stepping;
1089 Ok("Stepping to next operation...".to_string())
1090 }
1091
1092 DebugCommand::Continue => {
1093 *self.state.write() = DebuggerState::Continuing;
1094 Ok("Continuing execution...".to_string())
1095 }
1096
1097 DebugCommand::Run => {
1098 *self.state.write() = DebuggerState::Running;
1099 Ok("Running to completion...".to_string())
1100 }
1101
1102 DebugCommand::Pause => {
1103 *self.state.write() = DebuggerState::Paused;
1104 Ok("Paused".to_string())
1105 }
1106
1107 DebugCommand::ShowCallStack => {
1108 let context = self.context.lock();
1109 let mut output = String::from("Call Stack:\n");
1110
1111 for (i, op) in context.call_stack.iter().enumerate() {
1112 output.push_str(&format!(" #{}: {}\n", i, op));
1113 }
1114
1115 Ok(output)
1116 }
1117
1118 DebugCommand::ShowMemory => {
1119 let context = self.context.lock();
1120 Ok(format!(
1121 "Current memory usage: {} bytes",
1122 context.memory_usage
1123 ))
1124 }
1125
1126 DebugCommand::InspectTensor(tensor_id) => {
1127 let context = self.context.lock();
1128 if let Some(desc) = context.tensor_values.get(&tensor_id) {
1129 Ok(format!("Tensor {}: {}", tensor_id, desc))
1130 } else {
1131 Ok(format!("Tensor {} not found in current context", tensor_id))
1132 }
1133 }
1134
1135 DebugCommand::InspectGradient(tensor_id) => {
1136 let context = self.context.lock();
1137 if let Some(desc) = context.gradient_values.get(&tensor_id) {
1138 Ok(format!("Gradient for {}: {}", tensor_id, desc))
1139 } else {
1140 Ok(format!("Gradient for {} not found", tensor_id))
1141 }
1142 }
1143
1144 DebugCommand::ListBreakpoints => {
1145 let breakpoints = self.list_breakpoints();
1146 let mut output = String::from("Breakpoints:\n");
1147
1148 for bp in breakpoints {
1149 output.push_str(&format!(
1150 " #{}: {} [{}] (hits: {})\n",
1151 bp.id,
1152 bp.description,
1153 if bp.enabled { "enabled" } else { "disabled" },
1154 bp.hit_count
1155 ));
1156 }
1157
1158 Ok(output)
1159 }
1160
1161 DebugCommand::ListWatchpoints => {
1162 let watchpoints = self.list_watchpoints();
1163 let mut output = String::from("Watchpoints:\n");
1164
1165 for wp in watchpoints {
1166 output.push_str(&format!(
1167 " #{}: {} (triggers: {})\n",
1168 wp.id, wp.tensor_id, wp.trigger_count
1169 ));
1170 }
1171
1172 Ok(output)
1173 }
1174
1175 DebugCommand::Restart => {
1176 self.stop_debugging();
1177 Ok("Debugger restarted".to_string())
1178 }
1179
1180 DebugCommand::StepOver => self.step_over(),
1181
1182 DebugCommand::StepOut => self.step_out(),
1183 }
1184 }
1185
1186 fn snapshot_history(&self) -> Vec<HistoryNode> {
1188 self.history
1189 .lock()
1190 .iter()
1191 .map(|event| HistoryNode {
1192 id: event.id,
1193 parent_id: event.parent_id,
1194 operation: event.operation.clone(),
1195 })
1196 .collect()
1197 }
1198
1199 pub fn seek(&self, index: usize) -> AutogradResult<()> {
1210 let nodes = self.snapshot_history();
1211 if index >= nodes.len() {
1212 return Err(AutogradError::Configuration {
1213 parameter: "seek_index".to_string(),
1214 value: index.to_string(),
1215 reason: format!(
1216 "index out of range: {} recorded event(s) available",
1217 nodes.len()
1218 ),
1219 valid_range: if nodes.is_empty() {
1220 Some("no recorded events".to_string())
1221 } else {
1222 Some(format!("0..{}", nodes.len()))
1223 },
1224 });
1225 }
1226
1227 let call_stack = build_call_stack(&nodes, index);
1228 let mut context = self.context.lock();
1229 context.operation_index = index;
1230 context.call_stack = call_stack;
1231 Ok(())
1232 }
1233
1234 fn step_over(&self) -> AutogradResult<String> {
1242 let nodes = self.snapshot_history();
1243 if nodes.is_empty() {
1244 return Ok("No recorded execution to navigate".to_string());
1245 }
1246 let cur = self.context.lock().operation_index;
1247 if cur >= nodes.len() {
1248 return Ok(format!(
1249 "Already at end of recorded execution (index {cur}); nothing to step over"
1250 ));
1251 }
1252
1253 let parent_map: HashMap<TraceEventId, Option<TraceEventId>> =
1254 nodes.iter().map(|n| (n.id, n.parent_id)).collect();
1255 let current_id = nodes[cur].id;
1256 let new_pos = ((cur + 1)..nodes.len())
1257 .find(|&j| !is_descendant(nodes[j].id, current_id, &parent_map))
1258 .unwrap_or(nodes.len());
1259
1260 Ok(self.commit_navigation(&nodes, cur, new_pos, "Stepped over"))
1261 }
1262
1263 fn step_out(&self) -> AutogradResult<String> {
1269 let nodes = self.snapshot_history();
1270 if nodes.is_empty() {
1271 return Ok("No recorded execution to navigate".to_string());
1272 }
1273 let cur = self.context.lock().operation_index;
1274 if cur >= nodes.len() {
1275 return Ok(format!(
1276 "Already at end of recorded execution (index {cur}); nothing to step out of"
1277 ));
1278 }
1279
1280 match nodes[cur].parent_id {
1281 Some(parent_id) => {
1282 let parent_map: HashMap<TraceEventId, Option<TraceEventId>> =
1283 nodes.iter().map(|n| (n.id, n.parent_id)).collect();
1284 let new_pos = ((cur + 1)..nodes.len())
1285 .find(|&j| !is_descendant(nodes[j].id, parent_id, &parent_map))
1286 .unwrap_or(nodes.len());
1287 Ok(self.commit_navigation(&nodes, cur, new_pos, "Stepped out to"))
1288 }
1289 None => {
1290 let new_pos = nodes.len();
1292 let message = self.commit_navigation(&nodes, cur, new_pos, "Stepped out of");
1293 Ok(format!(
1294 "{message} (no enclosing frame: '{}' is a top-level operation)",
1295 nodes[cur].operation
1296 ))
1297 }
1298 }
1299 }
1300
1301 fn commit_navigation(
1304 &self,
1305 nodes: &[HistoryNode],
1306 cur: usize,
1307 new_pos: usize,
1308 verb: &str,
1309 ) -> String {
1310 let call_stack = if new_pos < nodes.len() {
1311 build_call_stack(nodes, new_pos)
1312 } else {
1313 Vec::new()
1314 };
1315
1316 {
1317 let mut context = self.context.lock();
1318 context.operation_index = new_pos;
1319 context.call_stack = call_stack;
1320 }
1321 *self.state.write() = DebuggerState::Paused;
1322
1323 if new_pos < nodes.len() {
1324 format!(
1325 "{verb} '{}' (event #{}) -> now at '{}' (event #{}, index {new_pos})",
1326 nodes[cur].operation, nodes[cur].id, nodes[new_pos].operation, nodes[new_pos].id
1327 )
1328 } else {
1329 format!(
1330 "{verb} '{}' (event #{}) -> reached end of recorded execution (index {new_pos})",
1331 nodes[cur].operation, nodes[cur].id
1332 )
1333 }
1334 }
1335
1336 pub fn context(&self) -> DebugContext {
1338 self.context.lock().clone()
1339 }
1340
1341 pub fn history(&self) -> Vec<TraceEvent> {
1343 self.history.lock().iter().cloned().collect()
1344 }
1345
1346 pub fn summary(&self) -> String {
1348 let context = self.context.lock();
1349 let breakpoints = self.breakpoints.lock();
1350 let watchpoints = self.watchpoints.lock();
1351
1352 let mut output = String::new();
1353
1354 output.push_str("=== Interactive Debugger Summary ===\n\n");
1355 output.push_str(&format!("State: {:?}\n", *self.state.read()));
1356 output.push_str(&format!(
1357 "Progress: {}/{} operations\n",
1358 context.operation_index, context.total_operations
1359 ));
1360 output.push_str(&format!("Memory usage: {} bytes\n", context.memory_usage));
1361 output.push_str(&format!(
1362 "Breakpoints: {} ({} enabled)\n",
1363 breakpoints.len(),
1364 breakpoints.values().filter(|b| b.enabled).count()
1365 ));
1366 output.push_str(&format!("Watchpoints: {}\n", watchpoints.len()));
1367
1368 output
1369 }
1370}
1371
1372impl Default for InteractiveDebugger {
1373 fn default() -> Self {
1374 Self::new()
1375 }
1376}
1377
1378static GLOBAL_DEBUGGER: once_cell::sync::Lazy<InteractiveDebugger> =
1380 once_cell::sync::Lazy::new(InteractiveDebugger::new);
1381
1382pub fn global_debugger() -> &'static InteractiveDebugger {
1384 &GLOBAL_DEBUGGER
1385}
1386
1387#[cfg(test)]
1388mod tests {
1389 use super::*;
1390
1391 #[test]
1392 fn test_debugger_creation() {
1393 let debugger = InteractiveDebugger::new();
1394 assert_eq!(debugger.state(), DebuggerState::Inactive);
1395 }
1396
1397 #[test]
1398 fn test_breakpoint_management() {
1399 let debugger = InteractiveDebugger::new();
1400
1401 let bp_id = debugger.add_breakpoint(
1402 BreakpointCondition::OperationName("matmul".to_string()),
1403 "Break on matmul".to_string(),
1404 );
1405
1406 assert!(bp_id > 0);
1407
1408 let breakpoints = debugger.list_breakpoints();
1409 assert_eq!(breakpoints.len(), 1);
1410
1411 debugger.disable_breakpoint(bp_id).unwrap();
1412
1413 let breakpoints = debugger.list_breakpoints();
1414 assert!(!breakpoints[0].enabled);
1415
1416 debugger.remove_breakpoint(bp_id).unwrap();
1417
1418 let breakpoints = debugger.list_breakpoints();
1419 assert_eq!(breakpoints.len(), 0);
1420 }
1421
1422 #[test]
1423 fn test_watchpoint_management() {
1424 let debugger = InteractiveDebugger::new();
1425
1426 let wp_id = debugger.add_watchpoint("tensor_1".to_string());
1427 assert!(wp_id > 0);
1428
1429 let watchpoints = debugger.list_watchpoints();
1430 assert_eq!(watchpoints.len(), 1);
1431
1432 debugger.remove_watchpoint(wp_id).unwrap();
1433
1434 let watchpoints = debugger.list_watchpoints();
1435 assert_eq!(watchpoints.len(), 0);
1436 }
1437
1438 #[test]
1439 fn test_command_execution() {
1440 let debugger = InteractiveDebugger::new();
1441
1442 let result = debugger.execute_command(DebugCommand::ShowMemory);
1443 assert!(result.is_ok());
1444
1445 let output = result.unwrap();
1446 assert!(output.contains("memory usage"));
1447 }
1448
1449 #[test]
1450 fn test_state_transitions() {
1451 let debugger = InteractiveDebugger::new();
1452
1453 debugger.execute_command(DebugCommand::Step).unwrap();
1454 assert_eq!(debugger.state(), DebuggerState::Stepping);
1455
1456 debugger.execute_command(DebugCommand::Continue).unwrap();
1457 assert_eq!(debugger.state(), DebuggerState::Continuing);
1458
1459 debugger.execute_command(DebugCommand::Pause).unwrap();
1460 assert_eq!(debugger.state(), DebuggerState::Paused);
1461 }
1462
1463 fn make_event(
1466 id: TraceEventId,
1467 parent_id: Option<TraceEventId>,
1468 operation: &str,
1469 event_type: EventType,
1470 ) -> TraceEvent {
1471 TraceEvent {
1472 id,
1473 parent_id,
1474 path_id: 1,
1475 event_type,
1476 operation: operation.to_string(),
1477 timestamp: chrono::Utc::now(),
1478 duration: None,
1479 memory_allocated: None,
1480 memory_deallocated: None,
1481 input_ids: Vec::new(),
1482 output_ids: Vec::new(),
1483 metadata: HashMap::new(),
1484 }
1485 }
1486
1487 #[test]
1490 fn test_l2_norm_from_csv_is_correct() {
1491 let norm = l2_norm_from_csv("3, 4").expect("should parse");
1493 assert!((norm - 5.0).abs() < 1e-9, "got {norm}");
1494
1495 let norm = l2_norm_from_csv("1,2,2").expect("should parse");
1497 assert!((norm - 3.0).abs() < 1e-9, "got {norm}");
1498
1499 assert!(l2_norm_from_csv("3, oops").is_none());
1501 assert!(l2_norm_from_csv("").is_none());
1502 }
1503
1504 #[test]
1505 fn test_extract_gradient_norm_from_metadata_and_context() {
1506 let context = DebugContext::new();
1507
1508 let mut event = make_event(1, None, "backward", EventType::GradientComputation);
1510 event
1511 .metadata
1512 .insert("gradient_norm".to_string(), "-7.5".to_string());
1513 let norm = extract_gradient_norm(&event, &context).expect("norm available");
1514 assert!((norm - 7.5).abs() < 1e-9, "got {norm}");
1515
1516 let mut event = make_event(2, None, "backward", EventType::GradientComputation);
1518 event
1519 .metadata
1520 .insert("gradient_values".to_string(), "6,8".to_string());
1521 let norm = extract_gradient_norm(&event, &context).expect("norm available");
1522 assert!((norm - 10.0).abs() < 1e-9, "got {norm}");
1523
1524 let mut context = DebugContext::new();
1527 context
1528 .gradient_values
1529 .insert("out_a".to_string(), "3,4".to_string());
1530 context
1531 .gradient_values
1532 .insert("out_b".to_string(), "norm=12".to_string());
1533 let mut event = make_event(3, None, "backward", EventType::BackwardEnd);
1534 event.output_ids = vec!["out_a".to_string(), "out_b".to_string()];
1535 let norm = extract_gradient_norm(&event, &context).expect("norm available");
1536 assert!((norm - 13.0).abs() < 1e-9, "got {norm}");
1537
1538 let mut forward = make_event(4, None, "forward", EventType::OperationBegin);
1540 forward.output_ids = vec!["out_a".to_string()];
1541 assert!(extract_gradient_norm(&forward, &context).is_none());
1542
1543 let bare = make_event(5, None, "backward", EventType::GradientComputation);
1545 assert!(extract_gradient_norm(&bare, &context).is_none());
1546 }
1547
1548 #[test]
1549 fn test_gradient_explosion_breakpoint_threshold_flag() {
1550 let context = DebugContext::new();
1551
1552 let mut bp = Breakpoint::new(
1553 1,
1554 BreakpointCondition::GradientExplosion(10.0),
1555 "explosion".to_string(),
1556 );
1557
1558 let mut small = make_event(1, None, "backward", EventType::GradientComputation);
1560 small
1561 .metadata
1562 .insert("gradient_values".to_string(), "3,4".to_string());
1563 assert!(!bp.should_trigger(&small, &context));
1564 assert_eq!(bp.hit_count, 0);
1565
1566 let mut big = make_event(2, None, "backward", EventType::GradientComputation);
1568 big.metadata
1569 .insert("gradient_values".to_string(), "30,40".to_string());
1570 assert!(bp.should_trigger(&big, &context));
1571 assert_eq!(bp.hit_count, 1);
1572
1573 let bare = make_event(3, None, "backward", EventType::GradientComputation);
1575 assert!(!bp.should_trigger(&bare, &context));
1576 assert_eq!(bp.hit_count, 1);
1577 }
1578
1579 #[test]
1580 fn test_gradient_vanishing_breakpoint_threshold_flag() {
1581 let context = DebugContext::new();
1582
1583 let mut bp = Breakpoint::new(
1584 1,
1585 BreakpointCondition::GradientVanishing(1e-3),
1586 "vanishing".to_string(),
1587 );
1588
1589 let mut healthy = make_event(1, None, "backward", EventType::GradientComputation);
1591 healthy
1592 .metadata
1593 .insert("gradient_values".to_string(), "3,4".to_string());
1594 assert!(!bp.should_trigger(&healthy, &context));
1595
1596 let mut tiny = make_event(2, None, "backward", EventType::GradientComputation);
1598 tiny.metadata
1599 .insert("gradient_norm".to_string(), "0.0001".to_string());
1600 assert!(bp.should_trigger(&tiny, &context));
1601 assert_eq!(bp.hit_count, 1);
1602 }
1603
1604 #[test]
1607 fn test_custom_expression_evaluates_known_values() {
1608 let mut event = make_event(1, None, "matmul", EventType::OperationBegin);
1609 event.memory_allocated = Some(2048);
1610 event.input_ids = vec!["a".to_string(), "b".to_string()];
1611 event.output_ids = vec!["c".to_string()];
1612
1613 let mut context = DebugContext::new();
1614 context.operation_index = 5;
1615 context.memory_usage = 1000;
1616
1617 assert!(evaluate_custom_expression("operation == matmul", &event, &context).unwrap());
1619 assert!(evaluate_custom_expression("operation == \"matmul\"", &event, &context).unwrap());
1620 assert!(!evaluate_custom_expression("operation == \"add\"", &event, &context).unwrap());
1621 assert!(evaluate_custom_expression("operation != add", &event, &context).unwrap());
1622
1623 assert!(evaluate_custom_expression("operation_index >= 5", &event, &context).unwrap());
1625 assert!(!evaluate_custom_expression("operation_index > 5", &event, &context).unwrap());
1626 assert!(evaluate_custom_expression("memory > 1000", &event, &context).unwrap());
1627 assert!(evaluate_custom_expression("input_count == 2", &event, &context).unwrap());
1628 assert!(evaluate_custom_expression("output_count == 1", &event, &context).unwrap());
1629
1630 assert!(
1632 evaluate_custom_expression("event_type == OperationBegin", &event, &context).unwrap()
1633 );
1634
1635 assert!(evaluate_custom_expression(
1637 "memory > 1000 && operation == matmul",
1638 &event,
1639 &context
1640 )
1641 .unwrap());
1642 assert!(!evaluate_custom_expression(
1643 "memory < 1000 && operation == matmul",
1644 &event,
1645 &context
1646 )
1647 .unwrap());
1648 assert!(evaluate_custom_expression(
1649 "memory < 1000 || operation_index == 5",
1650 &event,
1651 &context
1652 )
1653 .unwrap());
1654 assert!(evaluate_custom_expression(
1656 "operation == add && memory > 1000 || output_count == 1",
1657 &event,
1658 &context
1659 )
1660 .unwrap());
1661 }
1662
1663 #[test]
1664 fn test_custom_expression_gradient_norm_field() {
1665 let mut event = make_event(1, None, "backward", EventType::GradientComputation);
1666 event
1667 .metadata
1668 .insert("gradient_norm".to_string(), "50".to_string());
1669 let context = DebugContext::new();
1670
1671 assert!(evaluate_custom_expression("gradient_norm > 10", &event, &context).unwrap());
1672 assert!(!evaluate_custom_expression("gradient_norm < 10", &event, &context).unwrap());
1673
1674 let bare = make_event(2, None, "backward", EventType::GradientComputation);
1677 assert!(!evaluate_custom_expression("gradient_norm > 10", &bare, &context).unwrap());
1678 assert!(!evaluate_custom_expression("gradient_norm < 10", &bare, &context).unwrap());
1679 }
1680
1681 #[test]
1682 fn test_custom_expression_unsupported_inputs_error() {
1683 let event = make_event(1, None, "matmul", EventType::OperationBegin);
1684 let context = DebugContext::new();
1685
1686 assert!(evaluate_custom_expression("frobnicate == 5", &event, &context).is_err());
1688 assert!(evaluate_custom_expression(" ", &event, &context).is_err());
1690 assert!(evaluate_custom_expression("operation = matmul", &event, &context).is_err());
1692 assert!(evaluate_custom_expression("operation ==", &event, &context).is_err());
1694 assert!(evaluate_custom_expression("operation_index < matmul", &event, &context).is_err());
1696 assert!(evaluate_custom_expression("operation < matmul", &event, &context).is_err());
1698 assert!(evaluate_custom_expression("operation == matmul extra", &event, &context).is_err());
1700 }
1701
1702 #[test]
1703 fn test_custom_breakpoint_should_trigger() {
1704 let context = DebugContext::new();
1705 let mut bp = Breakpoint::new(
1706 1,
1707 BreakpointCondition::Custom("operation == matmul".to_string()),
1708 "custom".to_string(),
1709 );
1710
1711 let matmul = make_event(1, None, "matmul", EventType::OperationBegin);
1712 assert!(bp.should_trigger(&matmul, &context));
1713 assert_eq!(bp.hit_count, 1);
1714
1715 let add = make_event(2, None, "add", EventType::OperationBegin);
1716 assert!(!bp.should_trigger(&add, &context));
1717 assert_eq!(bp.hit_count, 1);
1718
1719 let mut broken = Breakpoint::new(
1721 2,
1722 BreakpointCondition::Custom("frobnicate == 5".to_string()),
1723 "broken".to_string(),
1724 );
1725 assert!(!broken.should_trigger(&matmul, &context));
1726 assert_eq!(broken.hit_count, 0);
1727 }
1728
1729 fn debugger_with_tree() -> InteractiveDebugger {
1742 let debugger = InteractiveDebugger::new();
1743 let events = [
1744 make_event(1, None, "forward", EventType::OperationBegin),
1745 make_event(2, Some(1), "layer1", EventType::OperationBegin),
1746 make_event(3, Some(2), "matmul", EventType::OperationBegin),
1747 make_event(4, Some(2), "add", EventType::OperationBegin),
1748 make_event(5, Some(1), "layer2", EventType::OperationBegin),
1749 make_event(6, Some(5), "matmul", EventType::OperationBegin),
1750 make_event(7, None, "backward", EventType::BackwardBegin),
1751 ];
1752 for event in &events {
1753 debugger.process_event(event).unwrap();
1754 }
1755 debugger
1756 }
1757
1758 #[test]
1759 fn test_step_over_skips_subtree() {
1760 let debugger = debugger_with_tree();
1761
1762 debugger.seek(1).unwrap();
1764 let msg = debugger.execute_command(DebugCommand::StepOver).unwrap();
1765 assert_eq!(debugger.context().operation_index, 4);
1767 assert_eq!(debugger.state(), DebuggerState::Paused);
1768 assert!(msg.contains("layer2"), "message was: {msg}");
1769
1770 debugger.seek(4).unwrap();
1772 debugger.execute_command(DebugCommand::StepOver).unwrap();
1773 assert_eq!(debugger.context().operation_index, 6);
1774 }
1775
1776 #[test]
1777 fn test_step_over_at_end_runs_to_end() {
1778 let debugger = debugger_with_tree();
1779 debugger.seek(6).unwrap();
1781 debugger.execute_command(DebugCommand::StepOver).unwrap();
1782 assert_eq!(debugger.context().operation_index, 7); }
1784
1785 #[test]
1786 fn test_step_out_returns_to_caller_frame() {
1787 let debugger = debugger_with_tree();
1788
1789 debugger.seek(2).unwrap();
1792 let msg = debugger.execute_command(DebugCommand::StepOut).unwrap();
1793 assert_eq!(debugger.context().operation_index, 4);
1794 assert_eq!(debugger.state(), DebuggerState::Paused);
1795 assert!(msg.contains("layer2"), "message was: {msg}");
1796
1797 debugger.seek(3).unwrap();
1799 debugger.execute_command(DebugCommand::StepOut).unwrap();
1800 assert_eq!(debugger.context().operation_index, 4);
1801 }
1802
1803 #[test]
1804 fn test_step_out_of_top_level_runs_to_end() {
1805 let debugger = debugger_with_tree();
1806 debugger.seek(0).unwrap();
1808 let msg = debugger.execute_command(DebugCommand::StepOut).unwrap();
1809 assert_eq!(debugger.context().operation_index, 7); assert!(
1811 msg.contains("top-level") || msg.contains("end of recorded execution"),
1812 "message was: {msg}"
1813 );
1814 }
1815
1816 #[test]
1817 fn test_seek_rebuilds_call_stack_and_validates_range() {
1818 let debugger = debugger_with_tree();
1819
1820 debugger.seek(2).unwrap();
1822 assert_eq!(
1823 debugger.context().call_stack,
1824 vec![
1825 "forward".to_string(),
1826 "layer1".to_string(),
1827 "matmul".to_string()
1828 ]
1829 );
1830
1831 assert!(debugger.seek(99).is_err());
1833 }
1834
1835 #[test]
1836 fn test_step_commands_on_empty_history() {
1837 let debugger = InteractiveDebugger::new();
1838 let over = debugger.execute_command(DebugCommand::StepOver).unwrap();
1839 let out = debugger.execute_command(DebugCommand::StepOut).unwrap();
1840 assert!(over.contains("No recorded execution"));
1841 assert!(out.contains("No recorded execution"));
1842 }
1843}