Skip to main content

trustformers_debug/
interactive_debugger.rs

1//! Interactive Debugger for TrustformeRS
2//!
3//! Provides step-through execution, breakpoints, variable inspection,
4//! call stack visualization, and time-travel debugging capabilities.
5// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
6// are retained for the data model, serialization completeness, and future consumers that
7// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
8#![allow(dead_code)]
9
10use anyhow::Result;
11use chrono::{DateTime, Utc};
12use indexmap::IndexMap;
13use parking_lot::{Mutex, RwLock};
14use serde::{Deserialize, Serialize};
15use std::collections::{HashMap, VecDeque};
16use std::sync::Arc;
17use uuid::Uuid;
18
19use crate::DebugConfig;
20
21/// Interactive debugger for step-through debugging and inspection
22#[derive(Debug)]
23pub struct InteractiveDebugger {
24    config: DebugConfig,
25    state: Arc<RwLock<DebuggerState>>,
26    breakpoints: Arc<RwLock<HashMap<String, Breakpoint>>>,
27    execution_history: Arc<Mutex<VecDeque<ExecutionSnapshot>>>,
28    current_step: Arc<Mutex<usize>>,
29    max_history_size: usize,
30}
31
32/// Current state of the debugger
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct DebuggerState {
35    pub is_running: bool,
36    pub is_paused: bool,
37    pub current_location: Option<DebugLocation>,
38    pub call_stack: Vec<StackFrame>,
39    pub variables: IndexMap<String, VariableValue>,
40    pub step_mode: StepMode,
41    pub session_start: DateTime<Utc>,
42}
43
44/// Debugging location identifier
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct DebugLocation {
47    pub module: String,
48    pub function: String,
49    pub line: Option<u32>,
50    pub instruction: Option<String>,
51    pub context: Option<String>,
52}
53
54/// Call stack frame
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct StackFrame {
57    pub id: Uuid,
58    pub location: DebugLocation,
59    pub locals: IndexMap<String, VariableValue>,
60    pub timestamp: DateTime<Utc>,
61    pub depth: usize,
62}
63
64/// Variable value with type information
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct VariableValue {
67    pub name: String,
68    pub value: String,
69    pub type_name: String,
70    pub size_bytes: Option<usize>,
71    pub shape: Option<Vec<usize>>,
72    pub is_tensor: bool,
73    pub metadata: HashMap<String, String>,
74}
75
76/// Execution step mode
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub enum StepMode {
79    /// Step into function calls
80    StepInto,
81    /// Step over function calls
82    StepOver,
83    /// Step out of current function
84    StepOut,
85    /// Continue until next breakpoint
86    Continue,
87    /// Single instruction step
88    SingleStep,
89}
90
91/// Breakpoint configuration
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct Breakpoint {
94    pub id: Uuid,
95    pub location: DebugLocation,
96    pub condition: Option<String>,
97    pub hit_count: usize,
98    pub enabled: bool,
99    pub temporary: bool,
100    pub log_message: Option<String>,
101    pub created_at: DateTime<Utc>,
102}
103
104/// Snapshot of execution state for time-travel debugging
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ExecutionSnapshot {
107    pub id: Uuid,
108    pub timestamp: DateTime<Utc>,
109    pub step_number: usize,
110    pub location: DebugLocation,
111    pub call_stack: Vec<StackFrame>,
112    pub variables: IndexMap<String, VariableValue>,
113    pub memory_usage: Option<usize>,
114    pub performance_metrics: HashMap<String, f64>,
115}
116
117/// Debugger command for external control
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub enum DebuggerCommand {
120    Start,
121    Pause,
122    Resume,
123    Step(StepMode),
124    SetBreakpoint(DebugLocation, Option<String>),
125    RemoveBreakpoint(Uuid),
126    InspectVariable(String),
127    EvaluateExpression(String),
128    ShowCallStack,
129    ShowHistory,
130    JumpToStep(usize),
131    Reset,
132    Exit,
133}
134
135/// Response from debugger operations
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub enum DebuggerResponse {
138    Started,
139    Paused(DebugLocation),
140    Resumed,
141    Stepped(ExecutionSnapshot),
142    BreakpointHit(Breakpoint, ExecutionSnapshot),
143    VariableInspected(VariableValue),
144    ExpressionEvaluated(String),
145    CallStackShown(Vec<StackFrame>),
146    HistoryShown(Vec<ExecutionSnapshot>),
147    JumpedToStep(ExecutionSnapshot),
148    Reset,
149    Error(String),
150}
151
152impl InteractiveDebugger {
153    /// Create a new interactive debugger
154    pub fn new(config: &DebugConfig) -> Self {
155        Self {
156            config: config.clone(),
157            state: Arc::new(RwLock::new(DebuggerState {
158                is_running: false,
159                is_paused: false,
160                current_location: None,
161                call_stack: Vec::new(),
162                variables: IndexMap::new(),
163                step_mode: StepMode::Continue,
164                session_start: Utc::now(),
165            })),
166            breakpoints: Arc::new(RwLock::new(HashMap::new())),
167            execution_history: Arc::new(Mutex::new(VecDeque::new())),
168            current_step: Arc::new(Mutex::new(0)),
169            max_history_size: config.max_gradient_history, // Reuse config value
170        }
171    }
172
173    /// Start the debugger
174    pub async fn start(&mut self) -> Result<()> {
175        let mut state = self.state.write();
176        state.is_running = true;
177        state.session_start = Utc::now();
178        tracing::info!("Interactive debugger started");
179        Ok(())
180    }
181
182    /// Process a debugger command
183    pub async fn process_command(&self, command: DebuggerCommand) -> Result<DebuggerResponse> {
184        match command {
185            DebuggerCommand::Start => {
186                let mut state = self.state.write();
187                state.is_running = true;
188                Ok(DebuggerResponse::Started)
189            },
190
191            DebuggerCommand::Pause => {
192                let mut state = self.state.write();
193                state.is_paused = true;
194                if let Some(location) = &state.current_location {
195                    Ok(DebuggerResponse::Paused(location.clone()))
196                } else {
197                    Ok(DebuggerResponse::Paused(DebugLocation {
198                        module: "unknown".to_string(),
199                        function: "unknown".to_string(),
200                        line: None,
201                        instruction: None,
202                        context: None,
203                    }))
204                }
205            },
206
207            DebuggerCommand::Resume => {
208                let mut state = self.state.write();
209                state.is_paused = false;
210                state.step_mode = StepMode::Continue;
211                Ok(DebuggerResponse::Resumed)
212            },
213
214            DebuggerCommand::Step(mode) => self.execute_step(mode).await,
215
216            DebuggerCommand::SetBreakpoint(location, condition) => {
217                self.set_breakpoint(location, condition).await
218            },
219
220            DebuggerCommand::RemoveBreakpoint(id) => self.remove_breakpoint(id).await,
221
222            DebuggerCommand::InspectVariable(name) => self.inspect_variable(&name).await,
223
224            DebuggerCommand::EvaluateExpression(expr) => self.evaluate_expression(&expr).await,
225
226            DebuggerCommand::ShowCallStack => {
227                let state = self.state.read();
228                Ok(DebuggerResponse::CallStackShown(state.call_stack.clone()))
229            },
230
231            DebuggerCommand::ShowHistory => {
232                let history = self.execution_history.lock();
233                Ok(DebuggerResponse::HistoryShown(
234                    history.iter().cloned().collect(),
235                ))
236            },
237
238            DebuggerCommand::JumpToStep(step_num) => self.jump_to_step(step_num).await,
239
240            DebuggerCommand::Reset => self.reset().await,
241
242            DebuggerCommand::Exit => {
243                let mut state = self.state.write();
244                state.is_running = false;
245                Ok(DebuggerResponse::Reset)
246            },
247        }
248    }
249
250    /// Execute a debugging step
251    async fn execute_step(&self, mode: StepMode) -> Result<DebuggerResponse> {
252        let mut state = self.state.write();
253        state.step_mode = mode;
254        state.is_paused = true;
255
256        // Create execution snapshot
257        let snapshot = ExecutionSnapshot {
258            id: Uuid::new_v4(),
259            timestamp: Utc::now(),
260            step_number: {
261                let mut step = self.current_step.lock();
262                *step += 1;
263                *step
264            },
265            location: state.current_location.clone().unwrap_or_else(|| DebugLocation {
266                module: "runtime".to_string(),
267                function: "step".to_string(),
268                line: None,
269                instruction: Some(format!("Step {:?}", mode)),
270                context: None,
271            }),
272            call_stack: state.call_stack.clone(),
273            variables: state.variables.clone(),
274            memory_usage: None,
275            performance_metrics: HashMap::new(),
276        };
277
278        // Add to history
279        {
280            let mut history = self.execution_history.lock();
281            history.push_back(snapshot.clone());
282            if history.len() > self.max_history_size {
283                history.pop_front();
284            }
285        }
286
287        Ok(DebuggerResponse::Stepped(snapshot))
288    }
289
290    /// Set a breakpoint
291    async fn set_breakpoint(
292        &self,
293        location: DebugLocation,
294        condition: Option<String>,
295    ) -> Result<DebuggerResponse> {
296        let breakpoint = Breakpoint {
297            id: Uuid::new_v4(),
298            location,
299            condition,
300            hit_count: 0,
301            enabled: true,
302            temporary: false,
303            log_message: None,
304            created_at: Utc::now(),
305        };
306
307        self.breakpoints.write().insert(breakpoint.id.to_string(), breakpoint.clone());
308
309        tracing::info!(
310            "Breakpoint set at {}::{}",
311            breakpoint.location.module,
312            breakpoint.location.function
313        );
314        Ok(DebuggerResponse::Started) // Use Started as generic success
315    }
316
317    /// Remove a breakpoint
318    async fn remove_breakpoint(&self, id: Uuid) -> Result<DebuggerResponse> {
319        if self.breakpoints.write().remove(&id.to_string()).is_some() {
320            tracing::info!("Breakpoint {} removed", id);
321        }
322        Ok(DebuggerResponse::Started)
323    }
324
325    /// Inspect a variable
326    async fn inspect_variable(&self, name: &str) -> Result<DebuggerResponse> {
327        let state = self.state.read();
328        if let Some(var) = state.variables.get(name) {
329            Ok(DebuggerResponse::VariableInspected(var.clone()))
330        } else {
331            Ok(DebuggerResponse::Error(format!(
332                "Variable '{}' not found",
333                name
334            )))
335        }
336    }
337
338    /// Evaluate a numeric watch expression against the current debugger
339    /// state (`state.variables`).
340    ///
341    /// Supports `+ - * / ( )`, numeric literals, and identifiers that
342    /// resolve to a currently-tracked variable whose `value` string parses
343    /// as `f64`. This is intentionally a minimal calculator, not a general
344    /// interpreter -- but every result it returns is a real evaluation of
345    /// the given expression against real debugger state. An expression this
346    /// evaluator cannot parse, or one that references an unknown variable
347    /// or a non-numeric one, returns [`DebuggerResponse::Error`] rather
348    /// than a fabricated "not implemented" success -- so a client UI never
349    /// displays a made-up value as if it were the real evaluated result.
350    async fn evaluate_expression(&self, expr: &str) -> Result<DebuggerResponse> {
351        let variables = self.state.read().variables.clone();
352        match ExpressionEvaluator::new(expr, &variables).evaluate() {
353            Ok(value) => Ok(DebuggerResponse::ExpressionEvaluated(format_eval_result(
354                value,
355            ))),
356            Err(message) => Ok(DebuggerResponse::Error(message)),
357        }
358    }
359
360    /// Jump to a specific step in history (time-travel debugging)
361    async fn jump_to_step(&self, step_num: usize) -> Result<DebuggerResponse> {
362        let history = self.execution_history.lock();
363        if let Some(snapshot) = history.iter().find(|s| s.step_number == step_num) {
364            let snapshot = snapshot.clone();
365            drop(history);
366
367            // Restore state from snapshot
368            {
369                let mut state = self.state.write();
370                state.current_location = Some(snapshot.location.clone());
371                state.call_stack = snapshot.call_stack.clone();
372                state.variables = snapshot.variables.clone();
373                state.is_paused = true;
374            }
375
376            *self.current_step.lock() = step_num;
377            Ok(DebuggerResponse::JumpedToStep(snapshot))
378        } else {
379            Ok(DebuggerResponse::Error(format!(
380                "Step {} not found in history",
381                step_num
382            )))
383        }
384    }
385
386    /// Reset the debugger state
387    pub async fn reset(&self) -> Result<DebuggerResponse> {
388        {
389            let mut state = self.state.write();
390            *state = DebuggerState {
391                is_running: false,
392                is_paused: false,
393                current_location: None,
394                call_stack: Vec::new(),
395                variables: IndexMap::new(),
396                step_mode: StepMode::Continue,
397                session_start: Utc::now(),
398            };
399        }
400
401        self.breakpoints.write().clear();
402        self.execution_history.lock().clear();
403        *self.current_step.lock() = 0;
404
405        Ok(DebuggerResponse::Reset)
406    }
407
408    /// Add a variable to the current scope
409    pub async fn add_variable(&self, name: String, value: String, type_name: String) -> Result<()> {
410        let var = VariableValue {
411            name: name.clone(),
412            value,
413            type_name,
414            size_bytes: None,
415            shape: None,
416            is_tensor: false,
417            metadata: HashMap::new(),
418        };
419
420        self.state.write().variables.insert(name, var);
421        Ok(())
422    }
423
424    /// Update current execution location
425    pub async fn update_location(&self, location: DebugLocation) -> Result<()> {
426        let mut state = self.state.write();
427        state.current_location = Some(location.clone());
428
429        // Check if we hit a breakpoint
430        let breakpoints = self.breakpoints.read();
431        for (_, breakpoint) in breakpoints.iter() {
432            if breakpoint.enabled
433                && breakpoint.location.module == location.module
434                && breakpoint.location.function == location.function
435            {
436                state.is_paused = true;
437                tracing::info!(
438                    "Breakpoint hit at {}::{}",
439                    location.module,
440                    location.function
441                );
442                break;
443            }
444        }
445
446        Ok(())
447    }
448
449    /// Push a new frame onto the call stack
450    pub async fn push_frame(&self, location: DebugLocation) -> Result<()> {
451        let frame = StackFrame {
452            id: Uuid::new_v4(),
453            location,
454            locals: IndexMap::new(),
455            timestamp: Utc::now(),
456            depth: self.state.read().call_stack.len(),
457        };
458
459        self.state.write().call_stack.push(frame);
460        Ok(())
461    }
462
463    /// Pop a frame from the call stack
464    pub async fn pop_frame(&self) -> Result<Option<StackFrame>> {
465        Ok(self.state.write().call_stack.pop())
466    }
467
468    /// Generate debugger report
469    pub async fn generate_report(&self) -> Result<InteractiveDebuggerReport> {
470        let state = self.state.read();
471        let breakpoints = self.breakpoints.read();
472        let history = self.execution_history.lock();
473
474        Ok(InteractiveDebuggerReport {
475            session_duration: Utc::now() - state.session_start,
476            total_steps: *self.current_step.lock(),
477            total_breakpoints: breakpoints.len(),
478            breakpoint_hits: breakpoints.values().map(|b| b.hit_count).sum(),
479            max_call_stack_depth: state.call_stack.len(),
480            variables_tracked: state.variables.len(),
481            history_entries: history.len(),
482            current_state: state.clone(),
483        })
484    }
485
486    /// Check if debugger is currently paused
487    pub fn is_paused(&self) -> bool {
488        self.state.read().is_paused
489    }
490
491    /// Get current step number
492    pub fn current_step(&self) -> usize {
493        *self.current_step.lock()
494    }
495
496    /// Get active breakpoints
497    pub fn get_breakpoints(&self) -> Vec<Breakpoint> {
498        self.breakpoints.read().values().cloned().collect()
499    }
500}
501
502/// Report generated by the interactive debugger
503#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct InteractiveDebuggerReport {
505    pub session_duration: chrono::Duration,
506    pub total_steps: usize,
507    pub total_breakpoints: usize,
508    pub breakpoint_hits: usize,
509    pub max_call_stack_depth: usize,
510    pub variables_tracked: usize,
511    pub history_entries: usize,
512    pub current_state: DebuggerState,
513}
514
515impl Default for DebuggerState {
516    fn default() -> Self {
517        Self {
518            is_running: false,
519            is_paused: false,
520            current_location: None,
521            call_stack: Vec::new(),
522            variables: IndexMap::new(),
523            step_mode: StepMode::Continue,
524            session_start: Utc::now(),
525        }
526    }
527}
528
529/// Formats a numeric evaluation result the way a debugger watch expression
530/// display should: integral values print without a trailing `.0` (`"3"`,
531/// not `"3.0"`), everything else prints with full `f64` precision.
532fn format_eval_result(value: f64) -> String {
533    if value.fract() == 0.0 && value.is_finite() && value.abs() < 1e15 {
534        format!("{}", value as i64)
535    } else {
536        format!("{value}")
537    }
538}
539
540/// A minimal recursive-descent parser/evaluator for numeric watch
541/// expressions over [`InteractiveDebugger`]'s tracked variables.
542///
543/// Grammar (standard precedence, left-associative `+ - * /`, right-assoc
544/// unary `-`):
545/// ```text
546/// expr   := term (('+' | '-') term)*
547/// term   := unary (('*' | '/') unary)*
548/// unary  := '-' unary | atom
549/// atom   := NUMBER | IDENT | '(' expr ')'
550/// ```
551struct ExpressionEvaluator<'a> {
552    tokens: Vec<Token>,
553    pos: usize,
554    variables: &'a IndexMap<String, VariableValue>,
555    source: &'a str,
556}
557
558#[derive(Debug, Clone, PartialEq)]
559enum Token {
560    Number(f64),
561    Ident(String),
562    Plus,
563    Minus,
564    Star,
565    Slash,
566    LParen,
567    RParen,
568}
569
570impl<'a> ExpressionEvaluator<'a> {
571    fn new(expr: &'a str, variables: &'a IndexMap<String, VariableValue>) -> Self {
572        Self {
573            tokens: Vec::new(),
574            pos: 0,
575            variables,
576            source: expr,
577        }
578    }
579
580    /// Parse and evaluate the expression, returning either the numeric
581    /// result or a human-readable error describing exactly what went wrong
582    /// (unknown variable, non-numeric variable, malformed syntax, division
583    /// by zero, or trailing unparsed input).
584    fn evaluate(mut self) -> Result<f64, String> {
585        self.tokenize()?;
586        if self.tokens.is_empty() {
587            return Err("empty expression".to_string());
588        }
589        let value = self.parse_expr()?;
590        if self.pos != self.tokens.len() {
591            return Err(format!(
592                "unexpected trailing input in expression {:?} at token {}",
593                self.source, self.pos
594            ));
595        }
596        Ok(value)
597    }
598
599    fn tokenize(&mut self) -> Result<(), String> {
600        let chars: Vec<char> = self.source.chars().collect();
601        let mut i = 0;
602        while i < chars.len() {
603            let c = chars[i];
604            match c {
605                ' ' | '\t' | '\n' | '\r' => {
606                    i += 1;
607                },
608                '+' => {
609                    self.tokens.push(Token::Plus);
610                    i += 1;
611                },
612                '-' => {
613                    self.tokens.push(Token::Minus);
614                    i += 1;
615                },
616                '*' => {
617                    self.tokens.push(Token::Star);
618                    i += 1;
619                },
620                '/' => {
621                    self.tokens.push(Token::Slash);
622                    i += 1;
623                },
624                '(' => {
625                    self.tokens.push(Token::LParen);
626                    i += 1;
627                },
628                ')' => {
629                    self.tokens.push(Token::RParen);
630                    i += 1;
631                },
632                c if c.is_ascii_digit() || c == '.' => {
633                    let start = i;
634                    while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
635                        i += 1;
636                    }
637                    let text: String = chars[start..i].iter().collect();
638                    let value = text.parse::<f64>().map_err(|_| {
639                        format!("invalid number literal {text:?} in {:?}", self.source)
640                    })?;
641                    self.tokens.push(Token::Number(value));
642                },
643                c if c.is_alphabetic() || c == '_' => {
644                    let start = i;
645                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
646                        i += 1;
647                    }
648                    let text: String = chars[start..i].iter().collect();
649                    self.tokens.push(Token::Ident(text));
650                },
651                other => {
652                    return Err(format!(
653                        "unsupported character {other:?} in expression {:?}",
654                        self.source
655                    ));
656                },
657            }
658        }
659        Ok(())
660    }
661
662    fn peek(&self) -> Option<&Token> {
663        self.tokens.get(self.pos)
664    }
665
666    fn advance(&mut self) -> Option<Token> {
667        let tok = self.tokens.get(self.pos).cloned();
668        if tok.is_some() {
669            self.pos += 1;
670        }
671        tok
672    }
673
674    fn parse_expr(&mut self) -> Result<f64, String> {
675        let mut value = self.parse_term()?;
676        loop {
677            match self.peek() {
678                Some(Token::Plus) => {
679                    self.advance();
680                    value += self.parse_term()?;
681                },
682                Some(Token::Minus) => {
683                    self.advance();
684                    value -= self.parse_term()?;
685                },
686                _ => break,
687            }
688        }
689        Ok(value)
690    }
691
692    fn parse_term(&mut self) -> Result<f64, String> {
693        let mut value = self.parse_unary()?;
694        loop {
695            match self.peek() {
696                Some(Token::Star) => {
697                    self.advance();
698                    value *= self.parse_unary()?;
699                },
700                Some(Token::Slash) => {
701                    self.advance();
702                    let divisor = self.parse_unary()?;
703                    if divisor == 0.0 {
704                        return Err(format!("division by zero in expression {:?}", self.source));
705                    }
706                    value /= divisor;
707                },
708                _ => break,
709            }
710        }
711        Ok(value)
712    }
713
714    fn parse_unary(&mut self) -> Result<f64, String> {
715        if matches!(self.peek(), Some(Token::Minus)) {
716            self.advance();
717            return Ok(-self.parse_unary()?);
718        }
719        self.parse_atom()
720    }
721
722    fn parse_atom(&mut self) -> Result<f64, String> {
723        match self.advance() {
724            Some(Token::Number(n)) => Ok(n),
725            Some(Token::Ident(name)) => {
726                let var = self.variables.get(&name).ok_or_else(|| {
727                    format!(
728                        "unknown variable {name:?} referenced in expression {:?}",
729                        self.source
730                    )
731                })?;
732                var.value.trim().parse::<f64>().map_err(|_| {
733                    format!(
734                        "variable {name:?} has non-numeric value {:?} and cannot be used in an \
735                         arithmetic expression",
736                        var.value
737                    )
738                })
739            },
740            Some(Token::LParen) => {
741                let value = self.parse_expr()?;
742                match self.advance() {
743                    Some(Token::RParen) => Ok(value),
744                    _ => Err(format!(
745                        "missing closing ')' in expression {:?}",
746                        self.source
747                    )),
748                }
749            },
750            other => Err(format!(
751                "unexpected token {other:?} in expression {:?}",
752                self.source
753            )),
754        }
755    }
756}
757
758#[cfg(test)]
759mod expression_evaluator_tests {
760    use super::*;
761
762    fn var(name: &str, value: &str) -> VariableValue {
763        VariableValue {
764            name: name.to_string(),
765            value: value.to_string(),
766            type_name: "f64".to_string(),
767            size_bytes: None,
768            shape: None,
769            is_tensor: false,
770            metadata: HashMap::new(),
771        }
772    }
773
774    #[test]
775    fn test_numeric_literal() {
776        let vars = IndexMap::new();
777        let result = ExpressionEvaluator::new("42", &vars).evaluate();
778        assert_eq!(result, Ok(42.0));
779    }
780
781    #[test]
782    fn test_basic_arithmetic_precedence() {
783        let vars = IndexMap::new();
784        // 2 + 3 * 4 = 14, not 20 -- confirms real operator precedence.
785        assert_eq!(
786            ExpressionEvaluator::new("2 + 3 * 4", &vars).evaluate(),
787            Ok(14.0)
788        );
789    }
790
791    #[test]
792    fn test_parentheses_override_precedence() {
793        let vars = IndexMap::new();
794        assert_eq!(
795            ExpressionEvaluator::new("(2 + 3) * 4", &vars).evaluate(),
796            Ok(20.0)
797        );
798    }
799
800    #[test]
801    fn test_unary_minus() {
802        let vars = IndexMap::new();
803        assert_eq!(
804            ExpressionEvaluator::new("-5 + 3", &vars).evaluate(),
805            Ok(-2.0)
806        );
807    }
808
809    #[test]
810    fn test_variable_lookup_resolves_real_value() {
811        let mut vars = IndexMap::new();
812        vars.insert("loss".to_string(), var("loss", "0.485"));
813        let result = ExpressionEvaluator::new("loss", &vars).evaluate();
814        assert_eq!(result, Ok(0.485));
815    }
816
817    #[test]
818    fn test_variable_in_arithmetic_expression() {
819        let mut vars = IndexMap::new();
820        vars.insert("grad_norm".to_string(), var("grad_norm", "2.5"));
821        let result = ExpressionEvaluator::new("grad_norm * 2", &vars).evaluate();
822        assert_eq!(result, Ok(5.0));
823    }
824
825    #[test]
826    fn test_unknown_variable_is_a_real_error_not_a_placeholder() {
827        let vars = IndexMap::new();
828        let result = ExpressionEvaluator::new("undefined_var", &vars).evaluate();
829        assert!(result.is_err());
830        assert!(result.expect_err("should be an error").contains("unknown variable"));
831    }
832
833    #[test]
834    fn test_non_numeric_variable_is_a_real_error() {
835        let mut vars = IndexMap::new();
836        vars.insert("model_name".to_string(), var("model_name", "gpt2"));
837        let result = ExpressionEvaluator::new("model_name", &vars).evaluate();
838        assert!(result.is_err());
839        assert!(result.expect_err("should be an error").contains("non-numeric"));
840    }
841
842    #[test]
843    fn test_division_by_zero_is_a_real_error() {
844        let vars = IndexMap::new();
845        let result = ExpressionEvaluator::new("1 / 0", &vars).evaluate();
846        assert!(result.is_err());
847        assert!(result.expect_err("should be an error").contains("division by zero"));
848    }
849
850    #[test]
851    fn test_malformed_expression_is_a_real_error() {
852        let vars = IndexMap::new();
853        assert!(ExpressionEvaluator::new("(1 + 2", &vars).evaluate().is_err());
854        assert!(ExpressionEvaluator::new("1 +", &vars).evaluate().is_err());
855        assert!(ExpressionEvaluator::new("1 2", &vars).evaluate().is_err());
856        assert!(ExpressionEvaluator::new("1 $ 2", &vars).evaluate().is_err());
857    }
858
859    #[test]
860    fn test_format_eval_result_integral_vs_fractional() {
861        assert_eq!(format_eval_result(3.0), "3");
862        assert_eq!(format_eval_result(3.5), "3.5");
863        assert_eq!(format_eval_result(-2.0), "-2");
864    }
865}
866
867#[cfg(test)]
868mod evaluate_expression_integration_tests {
869    use super::*;
870
871    fn make_debug_config() -> DebugConfig {
872        DebugConfig::default()
873    }
874
875    #[tokio::test]
876    async fn test_process_command_returns_error_for_unknown_variable_not_fake_success() {
877        let debugger = InteractiveDebugger::new(&make_debug_config());
878        let response = debugger
879            .process_command(DebuggerCommand::EvaluateExpression(
880                "nonexistent".to_string(),
881            ))
882            .await
883            .expect("process_command should not itself error");
884
885        // The old implementation returned `ExpressionEvaluated("Expression
886        // evaluation not implemented")` here -- a client would have
887        // displayed that placeholder text as the evaluated value.
888        match response {
889            DebuggerResponse::Error(message) => {
890                assert!(message.contains("unknown variable"));
891            },
892            other => panic!("expected DebuggerResponse::Error, got {other:?}"),
893        }
894    }
895
896    #[tokio::test]
897    async fn test_process_command_evaluates_real_arithmetic() {
898        let debugger = InteractiveDebugger::new(&make_debug_config());
899        let response = debugger
900            .process_command(DebuggerCommand::EvaluateExpression(
901                "(2 + 3) * 4".to_string(),
902            ))
903            .await
904            .expect("process_command should not error");
905
906        match response {
907            DebuggerResponse::ExpressionEvaluated(value) => assert_eq!(value, "20"),
908            other => panic!("expected DebuggerResponse::ExpressionEvaluated, got {other:?}"),
909        }
910    }
911
912    #[tokio::test]
913    async fn test_process_command_evaluates_expression_over_real_tracked_variable() {
914        let debugger = InteractiveDebugger::new(&make_debug_config());
915        {
916            let mut state = debugger.state.write();
917            state.variables.insert(
918                "batch_size".to_string(),
919                VariableValue {
920                    name: "batch_size".to_string(),
921                    value: "32".to_string(),
922                    type_name: "usize".to_string(),
923                    size_bytes: None,
924                    shape: None,
925                    is_tensor: false,
926                    metadata: HashMap::new(),
927                },
928            );
929        }
930
931        let response = debugger
932            .process_command(DebuggerCommand::EvaluateExpression(
933                "batch_size * 2".to_string(),
934            ))
935            .await
936            .expect("process_command should not error");
937
938        match response {
939            DebuggerResponse::ExpressionEvaluated(value) => assert_eq!(value, "64"),
940            other => panic!("expected DebuggerResponse::ExpressionEvaluated, got {other:?}"),
941        }
942    }
943}