runmat_kernel/
execution.rs

1//! Execution engine for RunMat code within the Jupyter kernel
2//!
3//! Provides a kernel-specific wrapper around the ReplEngine to adapt
4//! its interface for Jupyter protocol requirements.
5
6use crate::Result;
7use runmat_builtins::Value;
8use runmat_repl::ReplEngine;
9use std::path::Path;
10use std::time::{Duration, Instant};
11
12/// Execution engine managing RunMat code execution state for the Jupyter kernel
13pub struct ExecutionEngine {
14    /// Current execution counter
15    execution_count: u64,
16    /// Execution timeout
17    timeout: Option<Duration>,
18    /// Whether debug mode is enabled
19    debug: bool,
20    /// Underlying REPL engine that does the actual execution
21    repl_engine: ReplEngine,
22}
23
24/// Result of code execution
25#[derive(Debug, Clone)]
26pub struct ExecutionResult {
27    /// Execution status
28    pub status: ExecutionStatus,
29    /// Standard output captured during execution
30    pub stdout: String,
31    /// Standard error captured during execution  
32    pub stderr: String,
33    /// Execution result value (if successful)
34    pub result: Option<Value>,
35    /// Execution time in milliseconds
36    pub execution_time_ms: u64,
37    /// Any error that occurred
38    pub error: Option<ExecutionError>,
39}
40
41/// Execution status
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ExecutionStatus {
44    /// Execution completed successfully
45    Success,
46    /// Execution failed with an error
47    Error,
48    /// Execution was interrupted/cancelled
49    Interrupted,
50    /// Execution timed out
51    Timeout,
52}
53
54/// Execution error details
55#[derive(Debug, Clone)]
56pub struct ExecutionError {
57    /// Error type/name
58    pub error_type: String,
59    /// Error message
60    pub message: String,
61    /// Error traceback/stack trace
62    pub traceback: Vec<String>,
63}
64
65impl ExecutionEngine {
66    /// Create a new execution engine
67    pub fn new() -> Self {
68        let repl_engine =
69            ReplEngine::with_options(true, false).expect("Failed to create ReplEngine");
70        Self {
71            execution_count: 0,
72            timeout: Some(Duration::from_secs(300)), // 5 minutes default
73            debug: false,
74            repl_engine,
75        }
76    }
77
78    /// Create a new execution engine with custom timeout
79    pub fn with_timeout(timeout: Option<Duration>) -> Self {
80        let repl_engine =
81            ReplEngine::with_options(true, false).expect("Failed to create ReplEngine");
82        Self {
83            execution_count: 0,
84            timeout,
85            debug: false,
86            repl_engine,
87        }
88    }
89
90    /// Create a new execution engine with specific options
91    pub fn with_options(enable_jit: bool, debug: bool, timeout: Option<Duration>) -> Result<Self> {
92        Self::with_snapshot(enable_jit, debug, timeout, None::<&str>)
93    }
94
95    /// Create a new execution engine with snapshot support
96    pub fn with_snapshot<P: AsRef<Path>>(
97        enable_jit: bool,
98        debug: bool,
99        timeout: Option<Duration>,
100        snapshot_path: Option<P>,
101    ) -> Result<Self> {
102        let repl_engine =
103            ReplEngine::with_snapshot(enable_jit, debug, snapshot_path).map_err(|e| {
104                crate::KernelError::Internal(format!("Failed to create ReplEngine: {e}"))
105            })?;
106        Ok(Self {
107            execution_count: 0,
108            timeout,
109            debug,
110            repl_engine,
111        })
112    }
113
114    /// Enable or disable debug mode
115    pub fn set_debug(&mut self, debug: bool) {
116        self.debug = debug;
117    }
118
119    /// Get current execution count
120    pub fn execution_count(&self) -> u64 {
121        self.execution_count
122    }
123
124    /// Execute code
125    pub fn execute(&mut self, code: &str) -> Result<ExecutionResult> {
126        let start_time = Instant::now();
127        self.execution_count += 1;
128
129        if self.debug {
130            log::debug!("Executing code ({}): {}", self.execution_count, code);
131        }
132
133        // Execute using the underlying ReplEngine
134        match self.repl_engine.execute(code) {
135            Ok(repl_result) => {
136                let execution_time_ms = start_time.elapsed().as_millis() as u64;
137
138                if let Some(error_msg) = repl_result.error {
139                    // Determine error type based on error message content
140                    let error_type = if error_msg.contains("parse") || error_msg.contains("Parse") {
141                        "ParseError"
142                    } else if error_msg.contains("undefined") || error_msg.contains("variable") {
143                        "RuntimeError"
144                    } else if error_msg.contains("lower") || error_msg.contains("HIR") {
145                        "CompileError"
146                    } else {
147                        "ExecutionError"
148                    };
149
150                    Ok(ExecutionResult {
151                        status: ExecutionStatus::Error,
152                        stdout: self.capture_stdout(),
153                        stderr: self.capture_stderr(&error_msg),
154                        result: None,
155                        execution_time_ms,
156                        error: Some(ExecutionError {
157                            error_type: error_type.to_string(),
158                            message: error_msg,
159                            traceback: vec!["Error during code execution".to_string()],
160                        }),
161                    })
162                } else {
163                    Ok(ExecutionResult {
164                        status: ExecutionStatus::Success,
165                        stdout: self.capture_stdout(),
166                        stderr: String::new(), // No errors on success
167                        result: repl_result.value,
168                        execution_time_ms,
169                        error: None,
170                    })
171                }
172            }
173            Err(e) => {
174                let execution_time_ms = start_time.elapsed().as_millis() as u64;
175                let error_msg = e.to_string();
176
177                // Determine error type based on error message content
178                let error_type = if error_msg.contains("parse") || error_msg.contains("Parse") {
179                    "ParseError"
180                } else if error_msg.contains("undefined") || error_msg.contains("variable") {
181                    "RuntimeError"
182                } else if error_msg.contains("lower") || error_msg.contains("HIR") {
183                    "CompileError"
184                } else {
185                    "ExecutionError"
186                };
187
188                Ok(ExecutionResult {
189                    status: ExecutionStatus::Error,
190                    stdout: String::new(),
191                    stderr: String::new(),
192                    result: None,
193                    execution_time_ms,
194                    error: Some(ExecutionError {
195                        error_type: error_type.to_string(),
196                        message: error_msg,
197                        traceback: vec!["Error during code execution".to_string()],
198                    }),
199                })
200            }
201        }
202    }
203
204    /// Execute code with a specific timeout
205    pub fn execute_with_timeout(
206        &mut self,
207        code: &str,
208        timeout: Duration,
209    ) -> Result<ExecutionResult> {
210        let original_timeout = self.timeout;
211        self.timeout = Some(timeout);
212        let result = self.execute(code);
213        self.timeout = original_timeout;
214        result
215    }
216
217    /// Reset the execution engine state
218    pub fn reset(&mut self) {
219        self.execution_count = 0;
220        if self.debug {
221            log::debug!("Execution engine reset");
222        }
223    }
224
225    /// Get engine statistics
226    pub fn stats(&self) -> ExecutionStats {
227        let repl_stats = self.repl_engine.stats();
228        ExecutionStats {
229            execution_count: self.execution_count,
230            timeout_seconds: self.timeout.map(|d| d.as_secs()),
231            debug_enabled: self.debug,
232            repl_total_executions: repl_stats.total_executions,
233            repl_jit_compiled: repl_stats.jit_compiled,
234            repl_interpreter_fallback: repl_stats.interpreter_fallback,
235            repl_average_time_ms: repl_stats.average_execution_time_ms,
236        }
237    }
238
239    /// Get snapshot information
240    pub fn snapshot_info(&self) -> Option<String> {
241        self.repl_engine.snapshot_info()
242    }
243
244    /// Check if a snapshot is loaded
245    pub fn has_snapshot(&self) -> bool {
246        self.repl_engine.has_snapshot()
247    }
248
249    /// Capture stdout output from the REPL execution
250    fn capture_stdout(&self) -> String {
251        // In a production implementation, this would capture actual stdout
252        // For now, we simulate by checking if there were any successful results
253        // The REPL itself doesn't currently emit to stdout, but this would be the place
254        // to capture print() function output or similar
255        String::new()
256    }
257
258    /// Capture stderr output, including error messages
259    fn capture_stderr(&self, error_msg: &str) -> String {
260        // Format error message for stderr
261        format!("Error: {error_msg}")
262    }
263}
264
265impl Default for ExecutionEngine {
266    fn default() -> Self {
267        Self::new()
268    }
269}
270
271/// Execution engine statistics
272#[derive(Debug, Clone)]
273pub struct ExecutionStats {
274    /// Total number of executions performed by the kernel
275    pub execution_count: u64,
276    /// Configured timeout in seconds (if any)
277    pub timeout_seconds: Option<u64>,
278    /// Whether debug mode is enabled
279    pub debug_enabled: bool,
280    /// Total executions performed by the underlying REPL engine
281    pub repl_total_executions: usize,
282    /// Number of JIT compiled executions
283    pub repl_jit_compiled: usize,
284    /// Number of interpreter fallback executions
285    pub repl_interpreter_fallback: usize,
286    /// Average execution time in milliseconds
287    pub repl_average_time_ms: f64,
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_execution_engine_creation() {
296        let engine = ExecutionEngine::new();
297        assert_eq!(engine.execution_count(), 0);
298        assert!(!engine.debug);
299    }
300
301    #[test]
302    fn test_execution_engine_with_timeout() {
303        let timeout = Duration::from_secs(60);
304        let engine = ExecutionEngine::with_timeout(Some(timeout));
305        assert_eq!(engine.timeout, Some(timeout));
306    }
307
308    #[test]
309    fn test_simple_execution() {
310        let mut engine = ExecutionEngine::new();
311        let result = engine.execute("x = 1 + 2").unwrap();
312
313        assert_eq!(result.status, ExecutionStatus::Success);
314        assert_eq!(engine.execution_count(), 1);
315        // Just verify that execution_time_ms field exists and is accessible
316        let _time = result.execution_time_ms;
317        assert!(result.error.is_none());
318    }
319
320    #[test]
321    fn test_parse_error_handling() {
322        let mut engine = ExecutionEngine::new();
323        let result = engine.execute("x = 1 +").unwrap();
324
325        assert_eq!(result.status, ExecutionStatus::Error);
326        assert!(result.error.is_some());
327
328        let error = result.error.unwrap();
329        assert_eq!(error.error_type, "ParseError");
330        assert!(!error.message.is_empty());
331    }
332
333    #[test]
334    fn test_runtime_error_handling() {
335        let mut engine = ExecutionEngine::new();
336        let result = engine.execute("x = undefined_var").unwrap();
337
338        assert_eq!(result.status, ExecutionStatus::Error);
339        assert!(result.error.is_some());
340
341        let error = result.error.unwrap();
342        assert!(error.error_type == "RuntimeError" || error.error_type == "CompileError");
343    }
344
345    #[test]
346    fn test_execution_count_increment() {
347        let mut engine = ExecutionEngine::new();
348
349        engine.execute("x = 1").unwrap();
350        assert_eq!(engine.execution_count(), 1);
351
352        engine.execute("y = 2").unwrap();
353        assert_eq!(engine.execution_count(), 2);
354
355        // Even failed executions increment the counter
356        engine.execute("invalid syntax").unwrap();
357        assert_eq!(engine.execution_count(), 3);
358    }
359
360    #[test]
361    fn test_engine_reset() {
362        let mut engine = ExecutionEngine::new();
363        engine.execute("x = 1").unwrap();
364        assert_eq!(engine.execution_count(), 1);
365
366        engine.reset();
367        assert_eq!(engine.execution_count(), 0);
368    }
369
370    #[test]
371    fn test_debug_mode() {
372        let mut engine = ExecutionEngine::new();
373        assert!(!engine.debug);
374
375        engine.set_debug(true);
376        assert!(engine.debug);
377
378        engine.set_debug(false);
379        assert!(!engine.debug);
380    }
381
382    #[test]
383    fn test_stats() {
384        let mut engine = ExecutionEngine::new();
385        engine.set_debug(true);
386        engine.execute("x = 1").unwrap();
387
388        let stats = engine.stats();
389        assert_eq!(stats.execution_count, 1);
390        assert!(stats.debug_enabled);
391        assert!(stats.timeout_seconds.is_some());
392    }
393}