Skip to main content

torsh_autograd/
interactive_debugger.rs

1// Copyright (c) 2025 ToRSh Project
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Interactive Gradient Computation Debugger
5//!
6//! This module provides an interactive debugging interface for gradient computations,
7//! allowing step-by-step execution, tensor inspection, and computation graph analysis.
8//!
9//! # Features
10//!
11//! - **Step-by-step Execution**: Execute operations one at a time
12//! - **Breakpoints**: Set breakpoints on specific operations or conditions
13//! - **Tensor Inspection**: Examine tensor values and gradients
14//! - **Call Stack**: View operation call stack
15//! - **Watchpoints**: Monitor specific tensors for changes
16//! - **Time Travel**: Step backward through execution history
17
18use 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/// Debugger state
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum DebuggerState {
29    /// Debugger is inactive
30    Inactive,
31    /// Debugger is paused (waiting for user input)
32    Paused,
33    /// Debugger is running
34    Running,
35    /// Debugger is stepping (will pause after next operation)
36    Stepping,
37    /// Debugger is continuing to next breakpoint
38    Continuing,
39}
40
41/// Breakpoint condition
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub enum BreakpointCondition {
44    /// Break on specific operation name
45    OperationName(String),
46
47    /// Break on specific tensor ID
48    TensorId(String),
49
50    /// Break on anomaly detection
51    Anomaly,
52
53    /// Break on memory threshold
54    MemoryThreshold(usize),
55
56    /// Break after N operations
57    OperationCount(usize),
58
59    /// Break on gradient explosion (norm exceeds threshold)
60    GradientExplosion(f64),
61
62    /// Break on gradient vanishing (norm below threshold)
63    GradientVanishing(f64),
64
65    /// Custom condition (evaluates to true if should break)
66    Custom(String),
67}
68
69/// Breakpoint
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Breakpoint {
72    /// Breakpoint ID
73    pub id: u64,
74
75    /// Breakpoint condition
76    pub condition: BreakpointCondition,
77
78    /// Whether breakpoint is enabled
79    pub enabled: bool,
80
81    /// Number of times this breakpoint has been hit
82    pub hit_count: usize,
83
84    /// Description
85    pub description: String,
86}
87
88impl Breakpoint {
89    /// Create a new breakpoint
90    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    /// Check if this breakpoint should trigger
101    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                // Trigger only when real gradient data is available and its L2
120                // norm exceeds the threshold. Missing gradient data never fires
121                // (we do not invent a value).
122                extract_gradient_norm(event, context).is_some_and(|norm| norm > *threshold)
123            }
124            BreakpointCondition::GradientVanishing(threshold) => {
125                // Trigger only when real gradient data is available and its L2
126                // norm is below the threshold.
127                extract_gradient_norm(event, context).is_some_and(|norm| norm < *threshold)
128            }
129            BreakpointCondition::Custom(expr) => {
130                // Evaluate the user expression against the live debug state. An
131                // unevaluable expression does not fire (and is surfaced via the
132                // public `evaluate_custom_expression`); it never fakes a hit.
133                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
151// ---------------------------------------------------------------------------
152// Gradient-norm extraction
153// ---------------------------------------------------------------------------
154
155/// Compute the L2 norm of the gradient associated with a trace event.
156///
157/// Gradient-norm breakpoints (`GradientExplosion` / `GradientVanishing`) and
158/// the `gradient_norm` expression field require a concrete numeric gradient to
159/// test against. This helper extracts that gradient from the data actually
160/// attached to the event / debug context, in the following priority order:
161///
162/// 1. `event.metadata["gradient_norm"]` -- a pre-computed L2 norm (its absolute
163///    value is used).
164/// 2. `event.metadata["gradient_values"]` -- comma-separated gradient
165///    components; the norm is computed as `sqrt(sum(x_i^2))`.
166/// 3. For gradient-related events only (`GradientComputation`, `BackwardBegin`,
167///    `BackwardEnd`), each of the event's output tensors is looked up in
168///    `context.gradient_values`. An entry may be either a `norm=<value>` string
169///    or comma-separated components. The returned value is the global norm
170///    across all output tensors (`sqrt(sum over tensors of tensor_norm^2)`).
171///
172/// Returns `None` when no parseable gradient data is available, so callers can
173/// decline to fire a breakpoint rather than fabricating a value.
174fn extract_gradient_norm(event: &TraceEvent, context: &DebugContext) -> Option<f64> {
175    // 1. Pre-computed norm carried explicitly on the event.
176    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    // 2. Raw gradient components carried explicitly on the event.
185    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    // 3. Per-output-tensor gradients tracked in the debug context. Restricted to
192    //    gradient-related events to avoid reading stale gradients on forward ops.
193    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
219/// Compute the L2 norm of a comma-separated list of finite floats.
220///
221/// Returns `None` if the string is empty, contains no numeric components, or
222/// contains any token that does not parse as a finite float (so we never treat
223/// malformed data as a valid zero gradient).
224fn 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
246/// Parse a per-tensor gradient descriptor into an L2 norm.
247///
248/// Supports either a `norm=<value>` shorthand or a comma-separated list of
249/// gradient components. Returns `None` when neither form parses.
250fn 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// ---------------------------------------------------------------------------
264// Custom breakpoint expression evaluation
265// ---------------------------------------------------------------------------
266
267/// Comparison operator in a custom breakpoint expression.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269enum CompareOp {
270    Eq,
271    Ne,
272    Lt,
273    Le,
274    Gt,
275    Ge,
276}
277
278/// Lexical token of a custom breakpoint expression.
279#[derive(Debug, Clone, PartialEq)]
280enum Token {
281    /// A bareword identifier (field name on the left, string literal on the right).
282    Ident(String),
283    /// A numeric literal.
284    Number(f64),
285    /// A quoted string literal.
286    Str(String),
287    /// A comparison operator.
288    Compare(CompareOp),
289    /// Logical AND (`&&`).
290    And,
291    /// Logical OR (`||`).
292    Or,
293}
294
295/// Resolved value of an expression field.
296enum FieldValue {
297    Number(f64),
298    Text(String),
299    /// The field is supported but no value is currently available (e.g. a
300    /// gradient norm with no recorded gradient). Comparisons evaluate to false.
301    Missing,
302}
303
304/// Right-hand-side literal of a comparison.
305enum Operand {
306    Number(f64),
307    Text(String),
308}
309
310/// Evaluate a custom breakpoint expression against the current debug state.
311///
312/// # Supported grammar
313///
314/// ```text
315/// expr        := and_expr ( "||" and_expr )*
316/// and_expr    := comparison ( "&&" comparison )*
317/// comparison  := field op literal
318/// op          := "==" | "!=" | "<" | "<=" | ">" | ">="
319/// literal     := number | "quoted string" | bareword
320/// ```
321///
322/// `&&` binds tighter than `||`; there is no parenthesisation. The right-hand
323/// side of every comparison is a literal, not another field.
324///
325/// # Supported fields
326///
327/// * `operation` (text) -- the event operation name
328/// * `event_type` (text) -- the event type, e.g. `OperationBegin`
329/// * `memory` / `memory_allocated` (number) -- bytes allocated by the event
330/// * `memory_deallocated` (number) -- bytes freed by the event
331/// * `memory_usage` (number) -- current total memory usage
332/// * `operation_index` (number) -- current operation index
333/// * `total_operations` (number) -- total operation count
334/// * `input_count` / `output_count` (number) -- tensor arity of the event
335/// * `gradient_norm` (number) -- L2 norm of the event gradient (see
336///   `extract_gradient_norm`); comparisons are false when unavailable
337/// * `duration_micros` (number) -- event duration in microseconds
338///
339/// Text fields support only `==` / `!=`. Numeric fields support all operators.
340///
341/// # Errors
342///
343/// Returns an [`AutogradError::Configuration`] describing the problem when the
344/// expression is empty, malformed, references an unknown field, or compares
345/// incompatible types -- never a fabricated boolean.
346pub 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
373/// Construct a descriptive expression error carrying the supported grammar.
374fn 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
390/// Tokenize a custom breakpoint expression.
391fn 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
527/// Recursive-descent evaluator for custom breakpoint expressions.
528struct 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    /// `expr := and_expr ( "||" and_expr )*`
538    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            // Both sides are always parsed (no side effects), so evaluating the
543            // right operand unconditionally is correct.
544            let rhs = self.parse_and()?;
545            result = result || rhs;
546        }
547        Ok(result)
548    }
549
550    /// `and_expr := comparison ( "&&" comparison )*`
551    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    /// `comparison := field op literal`
562    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    /// Resolve a field name to its current value.
606    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    /// Evaluate a single comparison.
640    fn compare(&self, field: FieldValue, op: CompareOp, operand: Operand) -> AutogradResult<bool> {
641        match field {
642            // Supported field, but no data available: the comparison is simply
643            // not satisfied (this is honest, not a fabricated value).
644            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
685/// Apply a comparison operator to two floats (NaN-safe; NaN comparisons are
686/// false). Uses `partial_cmp` to avoid direct float equality.
687fn 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// ---------------------------------------------------------------------------
702// Recorded-graph navigation (step over / step out)
703// ---------------------------------------------------------------------------
704
705/// Lightweight snapshot of a recorded event used for navigation.
706///
707/// Captures only the parent/child relationship and the operation name so the
708/// recorded execution tree can be traversed without holding the history lock.
709#[derive(Debug, Clone)]
710struct HistoryNode {
711    id: TraceEventId,
712    parent_id: Option<TraceEventId>,
713    operation: String,
714}
715
716/// Return whether `node` is a (transitive) descendant of `ancestor` in the
717/// recorded event tree. A node is never a descendant of itself.
718fn 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(&current) {
726        if *parent == ancestor {
727            return true;
728        }
729        current = *parent;
730        guard += 1;
731        // Defensive bound against a malformed (cyclic) parent chain.
732        if guard > parent_map.len() {
733            break;
734        }
735    }
736    false
737}
738
739/// Reconstruct the call stack (root-first list of operation names) for the
740/// event at `index` by walking its ancestor chain in the recorded tree.
741fn 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/// Watchpoint for monitoring tensor changes
764#[derive(Debug, Clone, Serialize, Deserialize)]
765pub struct Watchpoint {
766    /// Watchpoint ID
767    pub id: u64,
768
769    /// Tensor ID to watch
770    pub tensor_id: String,
771
772    /// Whether to break on read
773    pub break_on_read: bool,
774
775    /// Whether to break on write
776    pub break_on_write: bool,
777
778    /// Whether to break on gradient update
779    pub break_on_gradient: bool,
780
781    /// Number of times this watchpoint has been triggered
782    pub trigger_count: usize,
783}
784
785/// Debug context containing current execution state
786#[derive(Debug, Clone)]
787pub struct DebugContext {
788    /// Current operation index
789    pub operation_index: usize,
790
791    /// Total operations
792    pub total_operations: usize,
793
794    /// Current call stack
795    pub call_stack: Vec<String>,
796
797    /// Current memory usage
798    pub memory_usage: usize,
799
800    /// Tensor values (tensor_id -> description)
801    pub tensor_values: HashMap<String, String>,
802
803    /// Gradient values (tensor_id -> gradient_description)
804    pub gradient_values: HashMap<String, String>,
805}
806
807impl DebugContext {
808    /// Create a new debug context
809    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
827/// Interactive debugger for gradient computations
828pub struct InteractiveDebugger {
829    /// Debugger state
830    state: Arc<RwLock<DebuggerState>>,
831
832    /// Breakpoints
833    breakpoints: Arc<Mutex<HashMap<u64, Breakpoint>>>,
834
835    /// Watchpoints
836    watchpoints: Arc<Mutex<HashMap<u64, Watchpoint>>>,
837
838    /// Debug context
839    context: Arc<Mutex<DebugContext>>,
840
841    /// Execution history
842    history: Arc<Mutex<VecDeque<TraceEvent>>>,
843
844    /// Next breakpoint ID
845    next_breakpoint_id: Arc<Mutex<u64>>,
846
847    /// Next watchpoint ID
848    next_watchpoint_id: Arc<Mutex<u64>>,
849
850    /// Current path being debugged
851    current_path: Arc<Mutex<Option<PathId>>>,
852
853    /// Maximum history size
854    max_history_size: usize,
855
856    /// Command queue (for programmatic control)
857    #[allow(dead_code)]
858    command_queue: Arc<Mutex<VecDeque<DebugCommand>>>,
859}
860
861/// Debug command
862#[derive(Debug, Clone, Serialize, Deserialize)]
863pub enum DebugCommand {
864    /// Step to next operation
865    Step,
866
867    /// Continue execution
868    Continue,
869
870    /// Step over (skip into children)
871    StepOver,
872
873    /// Step out (return to parent)
874    StepOut,
875
876    /// Run to completion
877    Run,
878
879    /// Pause execution
880    Pause,
881
882    /// Restart from beginning
883    Restart,
884
885    /// Inspect tensor
886    InspectTensor(String),
887
888    /// Inspect gradient
889    InspectGradient(String),
890
891    /// Show call stack
892    ShowCallStack,
893
894    /// Show memory usage
895    ShowMemory,
896
897    /// List breakpoints
898    ListBreakpoints,
899
900    /// List watchpoints
901    ListWatchpoints,
902}
903
904impl InteractiveDebugger {
905    /// Create a new interactive debugger
906    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)), // Start IDs from 1
914            next_watchpoint_id: Arc::new(Mutex::new(1)), // Start IDs from 1
915            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    /// Start debugging a gradient path
922    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    /// Stop debugging
931    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    /// Get current state
938    pub fn state(&self) -> DebuggerState {
939        *self.state.read()
940    }
941
942    /// Add a breakpoint
943    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    /// Remove a breakpoint
958    pub fn remove_breakpoint(&self, id: u64) -> AutogradResult<()> {
959        self.breakpoints.lock().remove(&id);
960        Ok(())
961    }
962
963    /// Enable a breakpoint
964    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    /// Disable a breakpoint
979    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    /// List all breakpoints
994    pub fn list_breakpoints(&self) -> Vec<Breakpoint> {
995        self.breakpoints.lock().values().cloned().collect()
996    }
997
998    /// Add a watchpoint
999    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    /// Remove a watchpoint
1022    pub fn remove_watchpoint(&self, id: u64) -> AutogradResult<()> {
1023        self.watchpoints.lock().remove(&id);
1024        Ok(())
1025    }
1026
1027    /// List all watchpoints
1028    pub fn list_watchpoints(&self) -> Vec<Watchpoint> {
1029        self.watchpoints.lock().values().cloned().collect()
1030    }
1031
1032    /// Process a trace event (called during execution)
1033    pub fn process_event(&self, event: &TraceEvent) -> AutogradResult<bool> {
1034        // Add to history
1035        {
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        // Update context
1045        {
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        // Check breakpoints
1059        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        // Check state
1074        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    /// Execute a debug command
1085    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    /// Snapshot the recorded execution history for navigation.
1187    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    /// Move the navigation cursor to a specific recorded-event index.
1200    ///
1201    /// The cursor (stored as `DebugContext::operation_index`) identifies the
1202    /// "current" event that [`DebugCommand::StepOver`] and
1203    /// [`DebugCommand::StepOut`] navigate relative to. The debug context's call
1204    /// stack is rebuilt from the recorded graph to reflect the new position.
1205    ///
1206    /// # Errors
1207    ///
1208    /// Returns an error when `index` is out of range for the recorded history.
1209    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    /// Step over the current event: advance the cursor past the current node's
1235    /// entire subtree, landing on the next sibling or ancestor continuation.
1236    ///
1237    /// Operates on the recorded execution graph. Returns a description of the
1238    /// resulting position. When there is no current event (empty history or the
1239    /// cursor is already at the end) an honest message is returned rather than
1240    /// moving an imaginary cursor.
1241    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    /// Step out of the current frame: advance the cursor past the remainder of
1264    /// the current node's parent subtree, returning to the caller frame.
1265    ///
1266    /// Operates on the recorded execution graph. A top-level event (no parent)
1267    /// has no caller, so the cursor runs to the end of the recorded execution.
1268    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                // A top-level operation has no enclosing frame to return to.
1291                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    /// Apply a navigation result: update the cursor, rebuild the call stack,
1302    /// pause the debugger, and produce a human-readable description.
1303    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    /// Get debug context
1337    pub fn context(&self) -> DebugContext {
1338        self.context.lock().clone()
1339    }
1340
1341    /// Get execution history
1342    pub fn history(&self) -> Vec<TraceEvent> {
1343        self.history.lock().iter().cloned().collect()
1344    }
1345
1346    /// Generate debug summary
1347    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
1378/// Global interactive debugger instance
1379static GLOBAL_DEBUGGER: once_cell::sync::Lazy<InteractiveDebugger> =
1380    once_cell::sync::Lazy::new(InteractiveDebugger::new);
1381
1382/// Get the global debugger
1383pub 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    // -- test helpers -------------------------------------------------------
1464
1465    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    // -- (a) gradient-norm checking ----------------------------------------
1488
1489    #[test]
1490    fn test_l2_norm_from_csv_is_correct() {
1491        // 3-4-5 right triangle: ||(3, 4)|| == 5.
1492        let norm = l2_norm_from_csv("3, 4").expect("should parse");
1493        assert!((norm - 5.0).abs() < 1e-9, "got {norm}");
1494
1495        // ||(1, 2, 2)|| == 3.
1496        let norm = l2_norm_from_csv("1,2,2").expect("should parse");
1497        assert!((norm - 3.0).abs() < 1e-9, "got {norm}");
1498
1499        // Malformed data is rejected rather than treated as zero.
1500        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        // Pre-computed norm wins and is taken as absolute value.
1509        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        // Raw components are reduced to their L2 norm.
1517        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        // Per-output-tensor gradients are aggregated into a global norm
1525        // (sqrt(5^2 + 12^2) == 13) for gradient events only.
1526        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        // A non-gradient event does not read context gradients (no fabrication).
1539        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        // No data anywhere -> None.
1544        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        // norm == 5 (from 3,4): below threshold 10 -> no trigger.
1559        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        // norm == 50 (from 30,40): above threshold 10 -> trigger and count.
1567        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        // No gradient data -> never fires (does not fabricate a norm).
1574        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        // norm == 5: above threshold -> no trigger.
1590        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        // norm == 1e-4 (below 1e-3) -> trigger.
1597        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    // -- (b) custom expression evaluation -----------------------------------
1605
1606    #[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        // String equality, both quoted and bareword forms.
1618        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        // Numeric ordering.
1624        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        // event_type as a bareword.
1631        assert!(
1632            evaluate_custom_expression("event_type == OperationBegin", &event, &context).unwrap()
1633        );
1634
1635        // Logical AND / OR with correct precedence (&& binds tighter than ||).
1636        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        // false && true || true == (false && true) || true == true
1655        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        // Missing gradient data: the field is supported but evaluates false,
1675        // it does not error and does not fabricate a value.
1676        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        // Unknown field.
1687        assert!(evaluate_custom_expression("frobnicate == 5", &event, &context).is_err());
1688        // Empty expression.
1689        assert!(evaluate_custom_expression("   ", &event, &context).is_err());
1690        // Single '=' is not a valid operator.
1691        assert!(evaluate_custom_expression("operation = matmul", &event, &context).is_err());
1692        // Missing operand.
1693        assert!(evaluate_custom_expression("operation ==", &event, &context).is_err());
1694        // Type mismatch: numeric field vs non-numeric literal.
1695        assert!(evaluate_custom_expression("operation_index < matmul", &event, &context).is_err());
1696        // Ordering on a text field is unsupported.
1697        assert!(evaluate_custom_expression("operation < matmul", &event, &context).is_err());
1698        // Trailing garbage.
1699        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        // A malformed custom expression must never fire (no fabricated hit).
1720        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    // -- (c) step-over / step-out navigation --------------------------------
1730
1731    /// Build a debugger with a known recorded execution tree:
1732    /// ```text
1733    /// 1 forward            (root,  index 0)
1734    /// 2  layer1   parent 1 (index 1)
1735    /// 3   matmul  parent 2 (index 2)
1736    /// 4   add     parent 2 (index 3)
1737    /// 5  layer2   parent 1 (index 4)
1738    /// 6   matmul  parent 5 (index 5)
1739    /// 7 backward           (root,  index 6)
1740    /// ```
1741    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        // Positioned at "layer1" (index 1); its subtree is {matmul, add}.
1763        debugger.seek(1).unwrap();
1764        let msg = debugger.execute_command(DebugCommand::StepOver).unwrap();
1765        // Should land on "layer2" (index 4), skipping the subtree.
1766        assert_eq!(debugger.context().operation_index, 4);
1767        assert_eq!(debugger.state(), DebuggerState::Paused);
1768        assert!(msg.contains("layer2"), "message was: {msg}");
1769
1770        // From "layer2" (index 4), step over skips {matmul} -> "backward" (6).
1771        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        // Last event (index 6) has no following events.
1780        debugger.seek(6).unwrap();
1781        debugger.execute_command(DebugCommand::StepOver).unwrap();
1782        assert_eq!(debugger.context().operation_index, 7); // == history length
1783    }
1784
1785    #[test]
1786    fn test_step_out_returns_to_caller_frame() {
1787        let debugger = debugger_with_tree();
1788
1789        // Positioned at "matmul" (index 2) inside "layer1"; stepping out should
1790        // finish layer1's frame and land on its sibling "layer2" (index 4).
1791        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        // From "add" (index 3), also inside layer1 -> step out to "layer2" (4).
1798        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        // "forward" (index 0) is a top-level frame: no caller to return to.
1807        debugger.seek(0).unwrap();
1808        let msg = debugger.execute_command(DebugCommand::StepOut).unwrap();
1809        assert_eq!(debugger.context().operation_index, 7); // ran to end
1810        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        // matmul (index 2) is forward -> layer1 -> matmul.
1821        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        // Out-of-range seek is an honest error.
1832        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}