oxur_repl/eval/
context.rs

1// Evaluation context for REPL sessions
2//
3// Manages session state, tiered execution, and code caching.
4// Based on ODD-0026: Oxur REPL Evaluation Strategy.
5
6use crate::cache::ArtifactCache;
7use crate::compiler::CachedCompiler;
8use crate::eval::{output_capture::OutputCapturer, LispEvaluator, SexprEvaluator};
9use crate::executor::SubprocessExecutor;
10use crate::metrics::EvalMetrics;
11use crate::protocol::{ReplMode, SessionId};
12use crate::session::SessionDir;
13use crate::type_inference::TypeInference;
14use crate::wrapper::RustAstWrapper;
15use oxur_smap::SourcePos;
16use std::collections::HashMap;
17use std::sync::{Arc, Mutex};
18use std::time::Instant;
19use thiserror::Error;
20
21/// Evaluation errors
22///
23/// All errors include source position tracking using oxur-smap::SourcePos
24/// to enable precise error reporting back to the original Oxur source code.
25#[derive(Debug, Error)]
26pub enum EvalError {
27    /// Syntax error during parsing
28    #[error("Syntax error at {pos}: {msg}")]
29    SyntaxError { msg: String, pos: SourcePos },
30
31    /// Type error during compilation
32    #[error("Type error at {pos}: {msg}")]
33    TypeError { msg: String, pos: SourcePos },
34
35    /// Runtime evaluation error
36    #[error("Runtime error at {pos}: {msg}")]
37    RuntimeError { msg: String, pos: SourcePos },
38
39    /// Compilation failed
40    #[error("Compilation failed at {pos}: {msg}")]
41    CompilationError { msg: String, pos: SourcePos },
42
43    /// Unsupported operation for tier
44    #[error("Unsupported operation at {pos}: {msg}")]
45    UnsupportedOperation { msg: String, pos: SourcePos },
46}
47
48pub type Result<T> = std::result::Result<T, EvalError>;
49
50/// Execution tier used for evaluation
51///
52/// Three-tier model per ODD-0026 Section 2:
53/// - Tier 1: Interpret literal arithmetic only
54/// - Tier 2: Execute from already-loaded library (cache hit)
55/// - Tier 3: Compile and load (cache miss or first time)
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57#[non_exhaustive]
58pub enum ExecutionTier {
59    /// Tier 1: Calculator mode - interpret simple arithmetic (<1ms)
60    ///
61    /// Only handles literal arithmetic expressions with no variables.
62    /// Examples: `(+ 1 2)`, `(* 3 4)`, `(/ 10 2)`
63    Calculator,
64
65    /// Tier 2: Cached and loaded - execute from loaded library (~1-5ms)
66    ///
67    /// Code was previously compiled and the dynamic library is already
68    /// loaded in the subprocess. Just calls the function.
69    CachedLoaded,
70
71    /// Tier 3: Just-in-time - compile, cache, and load (~50-300ms)
72    ///
73    /// First time seeing this code or library not yet loaded.
74    /// Compiles to Rust, invokes cargo, loads dylib into subprocess.
75    JustInTime,
76}
77
78impl ExecutionTier {
79    /// Get the metrics label for this tier.
80    ///
81    /// Returns a static string suitable for use as a metrics label value.
82    pub fn as_label(&self) -> &'static str {
83        match self {
84            Self::Calculator => "calculator",
85            Self::CachedLoaded => "cached",
86            Self::JustInTime => "jit",
87        }
88    }
89
90    /// Get a human-readable display name for this tier.
91    pub fn display_name(&self) -> &'static str {
92        match self {
93            Self::Calculator => "Calculator",
94            Self::CachedLoaded => "Cached",
95            Self::JustInTime => "JIT",
96        }
97    }
98}
99
100/// Result of an evaluation
101#[derive(Debug, Clone, PartialEq)]
102pub struct EvalResult {
103    /// The resulting value as a string
104    pub value: String,
105
106    /// Execution tier used
107    pub tier: ExecutionTier,
108
109    /// Whether result came from cache
110    pub cached: bool,
111
112    /// Execution duration in milliseconds
113    pub duration_ms: u64,
114
115    /// Standard output captured during evaluation
116    pub stdout: Option<String>,
117
118    /// Standard error captured during evaluation
119    pub stderr: Option<String>,
120}
121
122/// Evaluation context for a REPL session
123///
124/// Manages session state, execution tiers, and caching.
125///
126/// # Architecture
127///
128/// Uses Arc<Mutex<>> for components that need shared mutable state:
129/// - ArtifactCache: Shared persistent cache across clones
130/// - CachedCompiler: Shared compilation state
131/// - SubprocessExecutor: Single subprocess per session (not cloned)
132/// - SessionDir: Shared session directory
133#[derive(Debug, Clone)]
134pub struct EvalContext {
135    /// Unique session identifier
136    session_id: SessionId,
137
138    /// Evaluation mode (Lisp or Sexpr)
139    mode: ReplMode,
140
141    /// Lisp evaluator for Tier 1 (calculator mode in Lisp mode)
142    lisp_eval: LispEvaluator,
143
144    /// S-expression evaluator for Tier 1 (calculator mode in Sexpr mode)
145    sexpr_eval: SexprEvaluator,
146
147    /// Cached compiled code (hash -> result)
148    cache: HashMap<String, String>,
149
150    /// Statistics collector for tracking execution metrics (shared)
151    stats_collector: Arc<Mutex<EvalMetrics>>,
152
153    /// Usage metrics collector for tracking command patterns (shared)
154    usage_metrics: Arc<Mutex<crate::metrics::UsageMetrics>>,
155
156    /// Artifact cache for compiled libraries (shared)
157    artifact_cache: Option<Arc<Mutex<ArtifactCache>>>,
158
159    /// Cached compiler (shared)
160    compiler: Option<Arc<Mutex<CachedCompiler>>>,
161
162    /// Subprocess executor (shared, single instance per session)
163    executor: Option<Arc<Mutex<SubprocessExecutor>>>,
164
165    /// Session directory (shared)
166    session_dir: Option<Arc<SessionDir>>,
167
168    /// Rust AST wrapper for REPL scaffolding
169    wrapper: RustAstWrapper,
170
171    /// Type inference engine (Phase 7)
172    type_inference: TypeInference,
173
174    /// Source map for error translation (Phase 4)
175    ///
176    /// Tracks transformations from original Oxur source through Surface Forms,
177    /// Core Forms, and Rust AST for precise error position mapping.
178    source_map: oxur_smap::SourceMap,
179}
180
181impl EvalContext {
182    /// Create a new evaluation context
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// use oxur_repl::eval::EvalContext;
188    /// use oxur_repl::protocol::{ReplMode, SessionId};
189    ///
190    /// let ctx = EvalContext::new(SessionId::new("session-1"), ReplMode::Lisp);
191    /// ```
192    pub fn new(session_id: SessionId, mode: ReplMode) -> Self {
193        Self {
194            session_id: session_id.clone(),
195            mode,
196            lisp_eval: LispEvaluator::new(),
197            sexpr_eval: SexprEvaluator::new(),
198            cache: HashMap::new(),
199            stats_collector: Arc::new(Mutex::new(EvalMetrics::new(session_id.as_str()))),
200            usage_metrics: Arc::new(Mutex::new(crate::metrics::UsageMetrics::new(
201                session_id.as_str(),
202            ))),
203            artifact_cache: None,
204            compiler: None,
205            executor: None,
206            session_dir: None,
207            wrapper: RustAstWrapper::new(),
208            type_inference: TypeInference::new(),
209            source_map: oxur_smap::SourceMap::new(),
210        }
211    }
212
213    /// Create a new evaluation context with full compilation pipeline
214    ///
215    /// Initializes all Tier 2/3 components for actual compilation and execution.
216    ///
217    /// # Errors
218    ///
219    /// Returns error if:
220    /// - Session directory cannot be created
221    /// - Artifact cache cannot be initialized
222    /// - Subprocess cannot be spawned
223    pub fn with_compilation(session_id: SessionId, mode: ReplMode) -> Result<Self> {
224        // Create session directory
225        let session_dir =
226            SessionDir::new(&session_id).map_err(|e| EvalError::CompilationError {
227                msg: format!("Failed to create session directory: {}", e),
228                pos: SourcePos::repl(1, 1, 1),
229            })?;
230        let session_dir = Arc::new(session_dir);
231
232        // Create artifact cache
233        let artifact_cache = ArtifactCache::new().map_err(|e| EvalError::CompilationError {
234            msg: format!("Failed to create artifact cache: {}", e),
235            pos: SourcePos::repl(1, 1, 1),
236        })?;
237        let artifact_cache = Arc::new(Mutex::new(artifact_cache));
238
239        // Create compiler with Arc<SessionDir>
240        let cache_clone = {
241            let guard = artifact_cache.lock().expect("Artifact cache mutex poisoned");
242            (*guard).clone()
243        };
244        let compiler = CachedCompiler::new(cache_clone, Arc::clone(&session_dir));
245        let compiler = Arc::new(Mutex::new(compiler));
246
247        // Create subprocess executor
248        let executor = SubprocessExecutor::new().map_err(|e| EvalError::CompilationError {
249            msg: format!("Failed to create subprocess executor: {}", e),
250            pos: SourcePos::repl(1, 1, 1),
251        })?;
252        let executor = Arc::new(Mutex::new(executor));
253
254        Ok(Self {
255            session_id: session_id.clone(),
256            mode,
257            lisp_eval: LispEvaluator::new(),
258            sexpr_eval: SexprEvaluator::new(),
259            cache: HashMap::new(),
260            stats_collector: Arc::new(Mutex::new(EvalMetrics::new(session_id.as_str()))),
261            usage_metrics: Arc::new(Mutex::new(crate::metrics::UsageMetrics::new(
262                session_id.as_str(),
263            ))),
264            artifact_cache: Some(artifact_cache),
265            compiler: Some(compiler),
266            executor: Some(executor),
267            session_dir: Some(session_dir),
268            wrapper: RustAstWrapper::new(),
269            type_inference: TypeInference::new(),
270            source_map: oxur_smap::SourceMap::new(),
271        })
272    }
273
274    /// Get the session ID
275    pub fn session_id(&self) -> &SessionId {
276        &self.session_id
277    }
278
279    /// Get the evaluation mode
280    pub fn mode(&self) -> ReplMode {
281        self.mode
282    }
283
284    /// Get execution statistics (legacy method for backward compatibility)
285    ///
286    /// Returns (tier1_count, tier2_count, cache_hits)
287    /// Note: tier2_count includes both CachedLoaded and JustInTime tiers for backward compatibility
288    pub fn stats(&self) -> (u64, u64, u64) {
289        let collector = self.stats_collector.lock().unwrap();
290        let cache_stats = collector.cache_stats();
291        // Approximate tier counts from percentiles
292        let tier1_count =
293            collector.percentiles(ExecutionTier::Calculator).map(|p| p.count as u64).unwrap_or(0);
294        let tier2_count =
295            collector.percentiles(ExecutionTier::CachedLoaded).map(|p| p.count as u64).unwrap_or(0);
296        let tier3_count =
297            collector.percentiles(ExecutionTier::JustInTime).map(|p| p.count as u64).unwrap_or(0);
298        // Combine tier2 and tier3 for backward compatibility with old ExecutionStats
299        (tier1_count, tier2_count + tier3_count, cache_stats.hits)
300    }
301
302    /// Get the statistics collector
303    ///
304    /// Returns a shared reference to the stats collector for detailed metrics access.
305    pub fn stats_collector(&self) -> Arc<Mutex<EvalMetrics>> {
306        Arc::clone(&self.stats_collector)
307    }
308
309    /// Get the usage metrics collector
310    ///
311    /// Returns a shared reference to the usage metrics collector for command frequency tracking.
312    pub fn usage_metrics(&self) -> Arc<Mutex<crate::metrics::UsageMetrics>> {
313        Arc::clone(&self.usage_metrics)
314    }
315
316    /// Clone this context into a new session
317    ///
318    /// Creates a new context with a different session ID but the same
319    /// cache and state. Resets execution statistics.
320    ///
321    /// Note: Shared components (Arc<Mutex<>>) are cloned (reference counted),
322    /// so they share state across clones.
323    pub fn clone_to(&self, new_session_id: SessionId) -> Self {
324        Self {
325            session_id: new_session_id.clone(),
326            mode: self.mode,
327            lisp_eval: LispEvaluator::new(),
328            sexpr_eval: SexprEvaluator::new(),
329            cache: self.cache.clone(),
330            stats_collector: Arc::new(Mutex::new(EvalMetrics::new(new_session_id.as_str()))),
331            usage_metrics: Arc::new(Mutex::new(crate::metrics::UsageMetrics::new(
332                new_session_id.as_str(),
333            ))),
334            artifact_cache: self.artifact_cache.clone(),
335            compiler: self.compiler.clone(),
336            executor: self.executor.clone(),
337            session_dir: self.session_dir.clone(),
338            wrapper: RustAstWrapper::new(),
339            type_inference: TypeInference::new(),
340            source_map: oxur_smap::SourceMap::new(), // Fresh source map for new session
341        }
342    }
343
344    /// Evaluate code in this context
345    ///
346    /// Uses tiered execution strategy:
347    /// - Tier 1 (Calculator): Simple arithmetic literals only
348    /// - Tier 2 (Cached Compilation): Everything else
349    /// - Hybrid: Calculator variables accessible in compiled code
350    ///
351    /// # Errors
352    ///
353    /// Returns `EvalError` if evaluation fails due to:
354    /// - `SyntaxError`: Invalid syntax in the input code
355    /// - `TypeError`: Type mismatch or invalid type operations
356    /// - `RuntimeError`: Runtime errors during execution
357    /// - `CompilationError`: Failed to compile to executable code
358    /// - `UnsupportedOperation`: Operation not supported in current tier
359    pub async fn eval(&mut self, code: impl AsRef<str>) -> Result<EvalResult> {
360        let code = code.as_ref();
361        let start = Instant::now();
362
363        // Reset source map for this evaluation
364        self.source_map = oxur_smap::SourceMap::new();
365
366        // Attempt Tier 1 (Calculator mode) first, with error awareness
367        match self.try_calculator_with_errors(code) {
368            Ok(Some(result)) => {
369                // Calculator mode succeeded
370                let duration = start.elapsed();
371                let duration_ms = duration.as_millis() as u64;
372
373                // Record stats
374                self.stats_collector.lock().unwrap().record(
375                    ExecutionTier::Calculator,
376                    false,
377                    duration,
378                );
379
380                return Ok(EvalResult {
381                    value: result,
382                    tier: ExecutionTier::Calculator,
383                    cached: false,
384                    duration_ms,
385                    stdout: None,
386                    stderr: None,
387                });
388            }
389            Err(e) => {
390                // Calculator mode had an actual error (e.g., def syntax error)
391                return Err(e);
392            }
393            Ok(None) => {
394                // Not calculator-eligible, fall through to compilation
395            }
396        }
397
398        // Fall through to Tier 2/3 (Compilation)
399        // Check if we have def variables that need to be injected
400        self.eval_tier2_with_variables(code, start).await
401    }
402
403    /// Try to evaluate using Tier 1 (Calculator mode)
404    ///
405    /// Dispatches to the appropriate evaluator based on mode:
406    /// - Lisp mode: Simple arithmetic with Lisp syntax
407    /// - Sexpr mode: Canonical s-expressions with keywords
408    ///
409    /// Returns Some(result) if successful, None if not calculator-eligible.
410    ///
411    /// NOTE: Replaced by try_calculator_with_errors but kept for reference
412    #[allow(dead_code)]
413    fn try_calculator(&mut self, code: &str) -> Option<String> {
414        match self.mode {
415            ReplMode::Lisp => self.lisp_eval.try_eval_calculator(code),
416            ReplMode::Sexpr => self.sexpr_eval.try_eval_calculator(code),
417        }
418    }
419
420    /// Try calculator mode with error awareness
421    ///
422    /// Returns:
423    /// - `Ok(Some(result))` if evaluation succeeded
424    /// - `Ok(None)` if not calculator-eligible (should fall through to compilation)
425    /// - `Err(error)` if there was an actual error (e.g., def syntax error)
426    fn try_calculator_with_errors(&mut self, code: &str) -> Result<Option<String>> {
427        match self.mode {
428            ReplMode::Lisp => self.lisp_eval.try_eval_with_errors(code),
429            ReplMode::Sexpr => Ok(self.sexpr_eval.try_eval_calculator(code)), // Sexpr mode doesn't have error-aware version yet
430        }
431    }
432
433    /// Evaluate using Tier 2/3 (Cached or JIT Compilation)
434    ///
435    /// Distinguishes between:
436    /// - Tier 2 (CachedLoaded): Library already loaded in subprocess, just execute
437    /// - Tier 3 (JustInTime): Need to compile and/or load library before executing
438    ///
439    /// HYBRID MODE: If def variables exist, they are injected into compiled code
440    async fn eval_tier2_with_variables(
441        &mut self,
442        code: &str,
443        start: Instant,
444    ) -> Result<EvalResult> {
445        // Generate cache key (hash of code)
446        let cache_key = self.hash_code(code);
447
448        // If executor exists, check if library is already loaded (Tier 2)
449        if let Some(executor) = &self.executor {
450            let is_loaded = executor.lock().expect("Executor mutex poisoned").is_loaded(&cache_key);
451
452            if is_loaded {
453                // Tier 2: Library already loaded, just execute
454                let exec_result =
455                    executor.lock().expect("Executor mutex poisoned").execute(&cache_key).map_err(
456                        |e| EvalError::RuntimeError {
457                            msg: format!("Execution failed: {}", e),
458                            pos: SourcePos::repl(1, 1, code.len() as u32),
459                        },
460                    )?;
461
462                let duration = start.elapsed();
463                let duration_ms = duration.as_millis() as u64;
464
465                // Record stats
466                self.stats_collector.lock().unwrap().record(
467                    ExecutionTier::CachedLoaded,
468                    true,
469                    duration,
470                );
471
472                let result_value = match exec_result {
473                    crate::executor::ExecutionResult::Success { output } => output,
474                    crate::executor::ExecutionResult::RuntimeError { message } => {
475                        return Err(EvalError::RuntimeError {
476                            msg: message,
477                            pos: SourcePos::repl(1, 1, code.len() as u32),
478                        });
479                    }
480                    crate::executor::ExecutionResult::Panic { location, message } => {
481                        return Err(EvalError::RuntimeError {
482                            msg: format!("Panic at {}: {}", location, message),
483                            pos: SourcePos::repl(1, 1, code.len() as u32),
484                        });
485                    }
486                };
487
488                // Cache the result for future reference
489                self.cache.insert(cache_key, result_value.clone());
490
491                return Ok(EvalResult {
492                    value: result_value,
493                    tier: ExecutionTier::CachedLoaded,
494                    cached: true,
495                    duration_ms,
496                    stdout: None,
497                    stderr: None,
498                });
499            }
500        } else {
501            // No executor - check string cache for simple caching (backward compatibility)
502            if let Some(cached_result) = self.cache.get(&cache_key) {
503                let duration = start.elapsed();
504                let duration_ms = duration.as_millis() as u64;
505
506                // Record stats
507                self.stats_collector.lock().unwrap().record(
508                    ExecutionTier::CachedLoaded,
509                    true,
510                    duration,
511                );
512
513                return Ok(EvalResult {
514                    value: cached_result.clone(),
515                    tier: ExecutionTier::CachedLoaded,
516                    cached: true,
517                    duration_ms,
518                    stdout: None,
519                    stderr: None,
520                });
521            }
522        }
523
524        // Tier 3: Need to compile and/or load library
525        // Full compilation pipeline: parse, expand, wrap, compile, load, execute
526        let (result, stdout, stderr) = self.compile_and_execute(code).await?;
527        let duration = start.elapsed();
528        let duration_ms = duration.as_millis() as u64;
529
530        // Record stats
531        self.stats_collector.lock().unwrap().record(ExecutionTier::JustInTime, false, duration);
532
533        // Cache the result
534        self.cache.insert(cache_key, result.clone());
535
536        Ok(EvalResult {
537            value: result,
538            tier: ExecutionTier::JustInTime,
539            cached: false,
540            duration_ms,
541            stdout,
542            stderr,
543        })
544    }
545
546    /// Compile and execute code using the full pipeline
547    ///
548    /// Tier 2/3 execution path:
549    /// 1. Parse code using mode-specific parser (Lisp or Sexpr)
550    /// 2. Convert to CoreForms (oxur-lang IR)
551    /// 3. Wrap with REPL scaffolding (RustAstWrapper)
552    /// 4. Compile to dynamic library (CachedCompiler)
553    /// 5. Load library into subprocess (SubprocessExecutor)
554    /// 6. Execute with output capture
555    ///
556    /// Returns (result_value, stdout, stderr)
557    async fn compile_and_execute(
558        &mut self,
559        code: &str,
560    ) -> Result<(String, Option<String>, Option<String>)> {
561        // Step 1: Parse code to CoreForms using mode-specific parser
562        // Record surface nodes in source map during parsing
563        let core_forms = match self.mode {
564            ReplMode::Lisp => {
565                // Use Lisp parser to parse and convert to CoreForms
566                let forms = self.lisp_eval.parse(code).map_err(|e| EvalError::SyntaxError {
567                    msg: format!("Lisp parse error: {}", e),
568                    pos: SourcePos::repl(1, 1, code.len() as u32),
569                })?;
570
571                // Record surface nodes (Phase 4 placeholder - will be done by parser in Phase 6+)
572                // For now, create one surface node per form
573                for (idx, _form) in forms.iter().enumerate() {
574                    let node = oxur_smap::new_node_id();
575                    let pos = oxur_smap::SourcePos::repl(1, 1, code.len() as u32);
576                    self.source_map.record_surface_node(node, pos);
577
578                    // In Phase 6+, the parser will provide actual NodeIds and positions
579                    // For now this is a placeholder to enable comment generation
580                    let _ = idx; // suppress unused warning
581                }
582
583                forms
584            }
585            ReplMode::Sexpr => {
586                // Use Sexpr parser to parse and convert to CoreForms
587                let form =
588                    self.sexpr_eval.parse_to_core(code).map_err(|e| EvalError::SyntaxError {
589                        msg: format!("S-expression parse error: {}", e),
590                        pos: SourcePos::repl(1, 1, code.len() as u32),
591                    })?;
592
593                // Record surface node (Phase 4 placeholder)
594                let node = oxur_smap::new_node_id();
595                let pos = oxur_smap::SourcePos::repl(1, 1, code.len() as u32);
596                self.source_map.record_surface_node(node, pos);
597
598                vec![form]
599            }
600        };
601
602        // Create output capturer for this execution
603        let capturer = OutputCapturer::new();
604
605        // Handle empty parse results (Parser is placeholder for now)
606        if core_forms.is_empty() {
607            // Parser returned empty - it's a placeholder
608            // For now, just acknowledge we received code
609            tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
610
611            let result = format!("compiled(placeholder, mode: {:?})", self.mode);
612            let output = capturer.get_output();
613            return Ok((result, output.stdout_option(), output.stderr_option()));
614        }
615
616        // Step 2: Check if compilation pipeline is available BEFORE lowering
617        // This allows fallback to placeholder mode for tests/contexts without full pipeline
618        let (compiler, executor) = match (&self.compiler, &self.executor) {
619            (Some(c), Some(e)) => (c, e),
620            _ => {
621                // Fall back to simulation if pipeline not initialized
622                tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
623                let result = format!("compiled(placeholder, mode: {:?})", self.mode);
624                let output = capturer.get_output();
625                return Ok((result, output.stdout_option(), output.stderr_option()));
626            }
627        };
628
629        // Step 3: Convert CoreForms to Rust source code
630        // For Lisp mode: Lower CoreForms → Rust AST → Rust source
631        // For Sexpr mode: The input is expected to be raw Rust code, pass through
632        let rust_source = match self.mode {
633            ReplMode::Lisp => {
634                // Use a block scope to ensure syn::File (not Send) is dropped before any await
635                // Create an empty SourceMap for REPL context (no source position tracking)
636                let source_map = oxur_smap::SourceMap::new();
637                let mut lowerer = oxur_comp::lowering::Lowerer::new(source_map);
638                let (rust_ast, _source_map) =
639                    lowerer.lower(core_forms).map_err(|e| EvalError::CompilationError {
640                        msg: format!("Lowering error: {}", e),
641                        pos: SourcePos::repl(1, 1, code.len() as u32),
642                    })?;
643
644                let codegen = oxur_comp::codegen::CodeGenerator::new();
645                codegen.generate(&rust_ast).map_err(|e| EvalError::CompilationError {
646                    msg: format!("Code generation error: {}", e),
647                    pos: SourcePos::repl(1, 1, code.len() as u32),
648                })?
649            }
650            ReplMode::Sexpr => {
651                // Sexpr mode passes raw Rust code directly to compilation
652                code.to_string()
653            }
654        };
655
656        // Generate cache key
657        let cache_key = self.hash_code(code);
658
659        // Step 3: Wrap generated Rust code with REPL scaffolding
660        // Pass SourceMap for Phase 4 error translation
661        //
662        // HYBRID MODE: If we have def variables, inject them as literals
663        let def_variables = match self.mode {
664            ReplMode::Lisp => self.lisp_eval.get_variables(),
665            ReplMode::Sexpr => vec![], // Sexpr mode doesn't support def yet
666        };
667
668        let wrapped_code = if def_variables.is_empty() {
669            // No def variables, use standard wrapping
670            self.wrapper.wrap(&cache_key, &rust_source, Some(&self.source_map)).map_err(|e| {
671                EvalError::CompilationError {
672                    msg: format!("Failed to wrap code: {}", e),
673                    pos: SourcePos::repl(1, 1, code.len() as u32),
674                }
675            })?
676        } else {
677            // EXPERIMENTAL: Inject def variables as literal declarations
678            // Generate: let varname: type = value;
679            let mut var_decls = String::new();
680            for (name, type_name) in &def_variables {
681                if let Some(value) = match self.mode {
682                    ReplMode::Lisp => self.lisp_eval.get_variable_value(name),
683                    ReplMode::Sexpr => None,
684                } {
685                    var_decls.push_str(&format!("    let {}: {} = {};\n", name, type_name, value));
686                }
687            }
688
689            // Prepend variable declarations to the Rust source
690            let rust_with_vars = format!("{}\n{}", var_decls, rust_source);
691
692            self.wrapper.wrap(&cache_key, &rust_with_vars, Some(&self.source_map)).map_err(|e| {
693                EvalError::CompilationError {
694                    msg: format!("Failed to wrap code: {}", e),
695                    pos: SourcePos::repl(1, 1, code.len() as u32),
696                }
697            })?
698        };
699
700        // Step 4: Compile to dynamic library
701        let lib_path = compiler
702            .lock()
703            .expect("Compiler mutex poisoned")
704            .compile(&cache_key, &wrapped_code, 2)
705            .map_err(|e| EvalError::CompilationError {
706                msg: format!("Compilation failed: {}", e),
707                pos: SourcePos::repl(1, 1, code.len() as u32),
708            })?;
709
710        // Step 5: Load library into subprocess
711        executor
712            .lock()
713            .expect("Executor mutex poisoned")
714            .load_library(&lib_path, &cache_key)
715            .map_err(|e| EvalError::CompilationError {
716                msg: format!("Failed to load library: {}", e),
717                pos: SourcePos::repl(1, 1, code.len() as u32),
718            })?;
719
720        // Step 6: Execute in subprocess
721        let exec_result =
722            executor.lock().expect("Executor mutex poisoned").execute(&cache_key).map_err(|e| {
723                EvalError::RuntimeError {
724                    msg: format!("Execution failed: {}", e),
725                    pos: SourcePos::repl(1, 1, code.len() as u32),
726                }
727            })?;
728
729        // Extract result and handle different execution outcomes
730        let (result, stdout, stderr) = match exec_result {
731            crate::executor::ExecutionResult::Success { output } => (output, None, None),
732            crate::executor::ExecutionResult::RuntimeError { message } => {
733                return Err(EvalError::RuntimeError {
734                    msg: message,
735                    pos: SourcePos::repl(1, 1, code.len() as u32),
736                });
737            }
738            crate::executor::ExecutionResult::Panic { location, message } => {
739                return Err(EvalError::RuntimeError {
740                    msg: format!("Panic at {}: {}", location, message),
741                    pos: SourcePos::repl(1, 1, code.len() as u32),
742                });
743            }
744        };
745
746        Ok((result, stdout, stderr))
747    }
748
749    /// Generate cache key from code
750    fn hash_code(&self, code: &str) -> String {
751        // Simple hash for now (in production, use a real hash function)
752        use std::collections::hash_map::DefaultHasher;
753        use std::hash::{Hash, Hasher};
754
755        let mut hasher = DefaultHasher::new();
756        code.hash(&mut hasher);
757        // Prefix with 'h' to make it a valid Rust identifier
758        format!("h{:x}", hasher.finish())
759    }
760
761    /// Clear the compilation cache
762    pub fn clear_cache(&mut self) {
763        self.cache.clear();
764    }
765
766    /// Get cache size
767    pub fn cache_size(&self) -> usize {
768        self.cache.len()
769    }
770
771    // ========================================================================
772    // Resource Statistics Methods
773    // ========================================================================
774
775    /// Get session directory statistics
776    ///
777    /// Returns information about files and disk usage in the session's temporary directory.
778    /// Returns None if the session directory is not initialized.
779    pub fn session_dir_stats(&self) -> Option<crate::session::DirStats> {
780        self.session_dir.as_ref().and_then(|dir| dir.get_stats().ok())
781    }
782
783    /// Get artifact cache statistics
784    ///
785    /// Returns detailed statistics about the global artifact cache including entry count,
786    /// total size, and oldest/newest entries. Returns None if artifact cache is not initialized.
787    pub fn artifact_cache_stats(&self) -> Option<crate::cache::CacheStats> {
788        self.artifact_cache.as_ref().map(|cache| cache.lock().unwrap().detailed_stats())
789    }
790
791    /// Get subprocess metrics snapshot.
792    ///
793    /// Returns metrics about the subprocess executor including restart count,
794    /// uptime, and last restart reason.
795    pub fn subprocess_stats(&self) -> Option<crate::metrics::SubprocessMetricsSnapshot> {
796        self.executor.as_ref().map(|exec| exec.lock().unwrap().metrics().snapshot())
797    }
798
799    // ========================================================================
800    // Type Inference Methods (Phase 7)
801    // ========================================================================
802
803    /// Infer types from Rust code
804    ///
805    /// Parses the provided Rust code and extracts type information for
806    /// all variable declarations. This can be used to implement the `:type`
807    /// REPL command.
808    ///
809    /// # Arguments
810    ///
811    /// * `code` - Rust source code to analyze
812    ///
813    /// # Returns
814    ///
815    /// Vector of (variable_name, type_info) pairs
816    ///
817    /// # Examples
818    ///
819    /// ```
820    /// use oxur_repl::eval::EvalContext;
821    /// use oxur_repl::protocol::{ReplMode, SessionId};
822    ///
823    /// let ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
824    ///
825    /// let code = r#"
826    ///     fn main() {
827    ///         let x: i32 = 42;
828    ///         let y = "hello";
829    ///     }
830    /// "#;
831    ///
832    /// let types = ctx.infer_types(code);
833    /// assert_eq!(types.len(), 2);
834    /// ```
835    pub fn infer_types(&self, code: impl AsRef<str>) -> Vec<(String, String, bool)> {
836        self.type_inference
837            .infer_from_code(code)
838            .into_iter()
839            .map(|(name, info)| (name, info.type_name, info.is_mutable))
840            .collect()
841    }
842
843    /// Register a variable's type information
844    ///
845    /// Manually register type information for a variable. This is useful for
846    /// tracking types across REPL evaluations.
847    ///
848    /// # Arguments
849    ///
850    /// * `name` - Variable name
851    /// * `type_name` - Type as a string (e.g., "i32", "String")
852    /// * `is_mutable` - Whether the variable is mutable
853    ///
854    /// # Examples
855    ///
856    /// ```
857    /// use oxur_repl::eval::EvalContext;
858    /// use oxur_repl::protocol::{ReplMode, SessionId};
859    ///
860    /// let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
861    /// ctx.register_variable_type("x", "i32", false);
862    ///
863    /// let type_name = ctx.get_variable_type("x");
864    /// assert_eq!(type_name, Some("i32".to_string()));
865    /// ```
866    pub fn register_variable_type(
867        &mut self,
868        name: impl Into<String>,
869        type_name: impl Into<String>,
870        is_mutable: bool,
871    ) {
872        use crate::type_inference::TypeInfo;
873        let type_info = TypeInfo::new(type_name).with_mutable(is_mutable);
874        self.type_inference.register_variable(name, type_info);
875    }
876
877    /// Get the type of a variable
878    ///
879    /// Returns the type name for a previously registered variable.
880    ///
881    /// # Arguments
882    ///
883    /// * `name` - Variable name
884    ///
885    /// # Returns
886    ///
887    /// Some(type_name) if variable is tracked, None otherwise
888    ///
889    /// # Examples
890    ///
891    /// ```
892    /// use oxur_repl::eval::EvalContext;
893    /// use oxur_repl::protocol::{ReplMode, SessionId};
894    ///
895    /// let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
896    /// ctx.register_variable_type("x", "i32", false);
897    ///
898    /// assert_eq!(ctx.get_variable_type("x"), Some("i32".to_string()));
899    /// assert_eq!(ctx.get_variable_type("y"), None);
900    /// ```
901    pub fn get_variable_type(&self, name: impl AsRef<str>) -> Option<String> {
902        self.type_inference.get_variable_type(name).ok().map(|info| info.type_name.clone())
903    }
904
905    /// Get all tracked variables with their types
906    ///
907    /// Returns a list of all variables currently tracked by the type inference
908    /// engine along with their type information.
909    ///
910    /// # Returns
911    ///
912    /// Vector of (variable_name, type_name, is_mutable) tuples
913    ///
914    /// # Examples
915    ///
916    /// ```
917    /// use oxur_repl::eval::EvalContext;
918    /// use oxur_repl::protocol::{ReplMode, SessionId};
919    ///
920    /// let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
921    /// ctx.register_variable_type("x", "i32", false);
922    /// ctx.register_variable_type("y", "String", true);
923    ///
924    /// let vars = ctx.get_all_variable_types();
925    /// assert_eq!(vars.len(), 2);
926    /// ```
927    pub fn get_all_variable_types(&self) -> Vec<(String, String, bool)> {
928        self.type_inference
929            .all_variables()
930            .map(|(name, info)| (name.clone(), info.type_name.clone(), info.is_mutable))
931            .collect()
932    }
933
934    /// Clear all tracked type information
935    ///
936    /// Removes all tracked variable types from the type inference engine.
937    /// Useful when starting a new REPL session or resetting state.
938    ///
939    /// # Examples
940    ///
941    /// ```
942    /// use oxur_repl::eval::EvalContext;
943    /// use oxur_repl::protocol::{ReplMode, SessionId};
944    ///
945    /// let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
946    /// ctx.register_variable_type("x", "i32", false);
947    /// assert_eq!(ctx.get_all_variable_types().len(), 1);
948    ///
949    /// ctx.clear_type_information();
950    /// assert_eq!(ctx.get_all_variable_types().len(), 0);
951    /// ```
952    pub fn clear_type_information(&mut self) {
953        self.type_inference.clear();
954    }
955}
956
957#[cfg(test)]
958mod tests {
959    use super::*;
960
961    #[test]
962    fn test_new_context() {
963        let ctx = EvalContext::new(SessionId::new("test-session"), ReplMode::Lisp);
964        assert_eq!(ctx.session_id(), &SessionId::new("test-session"));
965        assert_eq!(ctx.mode(), ReplMode::Lisp);
966
967        let (tier1, tier2, hits) = ctx.stats();
968        assert_eq!(tier1, 0);
969        assert_eq!(tier2, 0);
970        assert_eq!(hits, 0);
971    }
972
973    #[test]
974    fn test_clone_to() {
975        use std::time::Duration;
976
977        let mut ctx = EvalContext::new(SessionId::new("session-1"), ReplMode::Lisp);
978
979        // Record some stats in the original context
980        ctx.stats_collector.lock().unwrap().record(
981            ExecutionTier::Calculator,
982            false,
983            Duration::from_millis(1),
984        );
985
986        ctx.cache.insert("key".to_string(), "value".to_string());
987
988        let cloned = ctx.clone_to(SessionId::new("session-2"));
989        assert_eq!(cloned.session_id(), &SessionId::new("session-2"));
990        assert_eq!(cloned.cache.len(), 1);
991
992        // Stats should be reset
993        let (tier1, tier2, hits) = cloned.stats();
994        assert_eq!(tier1, 0);
995        assert_eq!(tier2, 0);
996        assert_eq!(hits, 0);
997    }
998
999    #[tokio::test]
1000    async fn test_eval_tier1_calculator() {
1001        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1002
1003        let result = ctx.eval("(+ 1 2)").await.unwrap();
1004        assert_eq!(result.value, "3");
1005        assert_eq!(result.tier, ExecutionTier::Calculator);
1006        assert!(!result.cached);
1007        assert!(result.duration_ms < 10); // Should be very fast
1008
1009        let (tier1, tier2, _) = ctx.stats();
1010        assert_eq!(tier1, 1);
1011        assert_eq!(tier2, 0);
1012    }
1013
1014    #[tokio::test]
1015    async fn test_eval_tier3_jit_compilation() {
1016        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1017
1018        let result = ctx.eval("(deffn foo (x) (* x 2))").await.unwrap();
1019        assert!(result.value.contains("compiled"));
1020        assert_eq!(result.tier, ExecutionTier::JustInTime);
1021        assert!(!result.cached);
1022
1023        let (tier1, tier2, _) = ctx.stats();
1024        assert_eq!(tier1, 0);
1025        assert_eq!(tier2, 1);
1026    }
1027
1028    #[tokio::test]
1029    async fn test_eval_caching() {
1030        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1031
1032        // First evaluation - not cached (Tier 3: JIT)
1033        let result1 = ctx.eval("(deffn bar (x) x)").await.unwrap();
1034        assert!(!result1.cached);
1035        assert_eq!(result1.tier, ExecutionTier::JustInTime);
1036
1037        // Second evaluation - should be cached (Tier 2: CachedLoaded)
1038        let result2 = ctx.eval("(deffn bar (x) x)").await.unwrap();
1039        assert!(result2.cached);
1040        assert_eq!(result2.tier, ExecutionTier::CachedLoaded);
1041        assert_eq!(result2.value, result1.value);
1042
1043        let (_, tier2, hits) = ctx.stats();
1044        assert_eq!(tier2, 2);
1045        assert_eq!(hits, 1);
1046    }
1047
1048    #[tokio::test]
1049    async fn test_cache_operations() {
1050        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1051
1052        ctx.eval("(+ x 1)").await.unwrap();
1053        assert_eq!(ctx.cache_size(), 1);
1054
1055        ctx.clear_cache();
1056        assert_eq!(ctx.cache_size(), 0);
1057    }
1058
1059    #[test]
1060    fn test_calculator_mode() {
1061        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1062
1063        assert_eq!(ctx.try_calculator("(+ 1 2)"), Some("3".to_string()));
1064        assert_eq!(ctx.try_calculator("(+ 10 20)"), Some("30".to_string()));
1065        assert_eq!(ctx.try_calculator("(deffn foo (x) x)"), None);
1066        assert_eq!(ctx.try_calculator("(+ 1 2 3)"), Some("6".to_string())); // Multiple args now supported
1067    }
1068
1069    #[tokio::test]
1070    async fn test_tier3_lisp_mode() {
1071        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1072
1073        // Code that's not calculator-eligible goes to Tier 3 (JIT) first time
1074        let result = ctx.eval("(deffn foo (x) x)").await.unwrap();
1075
1076        assert_eq!(result.tier, ExecutionTier::JustInTime);
1077        assert!(!result.cached);
1078        assert!(result.value.contains("mode: Lisp"));
1079    }
1080
1081    #[tokio::test]
1082    async fn test_tier3_sexpr_mode() {
1083        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Sexpr);
1084
1085        // Complex code goes to Tier 3 (JIT) first time (not handled by calculator)
1086        // Using an invalid symbol that can't be evaluated in calculator
1087        let result = ctx.eval("undefined-symbol").await.unwrap();
1088
1089        assert_eq!(result.tier, ExecutionTier::JustInTime);
1090        assert!(!result.cached);
1091        // Without compilation pipeline initialized, falls back to placeholder mode
1092        assert!(result.value.contains("placeholder"));
1093    }
1094
1095    #[tokio::test]
1096    async fn test_tier_fallback() {
1097        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1098
1099        // Tier 1 (calculator) - fast path
1100        let result1 = ctx.eval("(+ 1 2)").await.unwrap();
1101        assert_eq!(result1.tier, ExecutionTier::Calculator);
1102        assert_eq!(result1.value, "3");
1103
1104        // Tier 3 (JIT compilation) - complex code, first time
1105        let result2 = ctx.eval("(deffn add (a b) (+ a b))").await.unwrap();
1106        assert_eq!(result2.tier, ExecutionTier::JustInTime);
1107    }
1108
1109    #[tokio::test]
1110    async fn test_mode_specific_calculators() {
1111        // Test Lisp mode calculator
1112        let mut lisp_ctx = EvalContext::new(SessionId::new("lisp-test"), ReplMode::Lisp);
1113        let lisp_result = lisp_ctx.eval("(* 3 4)").await.unwrap();
1114        assert_eq!(lisp_result.tier, ExecutionTier::Calculator);
1115        assert_eq!(lisp_result.value, "12");
1116
1117        // Test Sexpr mode calculator
1118        let mut sexpr_ctx = EvalContext::new(SessionId::new("sexpr-test"), ReplMode::Sexpr);
1119        let sexpr_result = sexpr_ctx.eval("(/ 10 2)").await.unwrap();
1120        assert_eq!(sexpr_result.tier, ExecutionTier::Calculator);
1121        assert_eq!(sexpr_result.value, "5");
1122    }
1123
1124    #[test]
1125    fn test_source_pos_creation() {
1126        let pos = SourcePos::repl(3, 15, 10);
1127        assert_eq!(pos.line, 3);
1128        assert_eq!(pos.column, 15);
1129        assert_eq!(pos.length, 10);
1130        assert_eq!(pos.file, "<repl>");
1131    }
1132
1133    #[test]
1134    fn test_source_pos_display() {
1135        let pos = SourcePos::repl(3, 15, 10);
1136        assert_eq!(pos.to_string(), "<repl>:3:15");
1137    }
1138
1139    #[test]
1140    fn test_source_pos_equality() {
1141        let pos1 = SourcePos::repl(3, 15, 10);
1142        let pos2 = SourcePos::repl(3, 15, 10);
1143        let pos3 = SourcePos::repl(4, 10, 5);
1144
1145        assert_eq!(pos1, pos2);
1146        assert_ne!(pos1, pos3);
1147    }
1148
1149    #[test]
1150    fn test_eval_error_includes_source_pos() {
1151        let err = EvalError::SyntaxError {
1152            msg: "Unexpected token".to_string(),
1153            pos: SourcePos::repl(2, 5, 1),
1154        };
1155
1156        let err_string = err.to_string();
1157        assert!(err_string.contains("<repl>:2:5"));
1158        assert!(err_string.contains("Unexpected token"));
1159    }
1160
1161    #[test]
1162    fn test_all_error_variants_include_source_pos() {
1163        let pos = SourcePos::repl(1, 1, 1);
1164
1165        let errors = vec![
1166            EvalError::SyntaxError { msg: "syntax".to_string(), pos: pos.clone() },
1167            EvalError::TypeError { msg: "type".to_string(), pos: pos.clone() },
1168            EvalError::RuntimeError { msg: "runtime".to_string(), pos: pos.clone() },
1169            EvalError::CompilationError { msg: "compilation".to_string(), pos: pos.clone() },
1170            EvalError::UnsupportedOperation { msg: "unsupported".to_string(), pos: pos.clone() },
1171        ];
1172
1173        for err in errors {
1174            let err_string = err.to_string();
1175            assert!(err_string.contains("<repl>:1:1"));
1176        }
1177    }
1178
1179    // ===== Additional coverage tests =====
1180
1181    #[test]
1182    fn test_execution_tier_as_label() {
1183        assert_eq!(ExecutionTier::Calculator.as_label(), "calculator");
1184        assert_eq!(ExecutionTier::CachedLoaded.as_label(), "cached");
1185        assert_eq!(ExecutionTier::JustInTime.as_label(), "jit");
1186    }
1187
1188    #[test]
1189    fn test_execution_tier_display_name() {
1190        assert_eq!(ExecutionTier::Calculator.display_name(), "Calculator");
1191        assert_eq!(ExecutionTier::CachedLoaded.display_name(), "Cached");
1192        assert_eq!(ExecutionTier::JustInTime.display_name(), "JIT");
1193    }
1194
1195    #[test]
1196    fn test_eval_result_struct() {
1197        let result = EvalResult {
1198            value: "42".to_string(),
1199            tier: ExecutionTier::Calculator,
1200            cached: false,
1201            duration_ms: 5,
1202            stdout: Some("output".to_string()),
1203            stderr: Some("error".to_string()),
1204        };
1205
1206        assert_eq!(result.value, "42");
1207        assert_eq!(result.tier, ExecutionTier::Calculator);
1208        assert!(!result.cached);
1209        assert_eq!(result.duration_ms, 5);
1210        assert_eq!(result.stdout, Some("output".to_string()));
1211        assert_eq!(result.stderr, Some("error".to_string()));
1212    }
1213
1214    #[test]
1215    fn test_eval_result_equality() {
1216        let result1 = EvalResult {
1217            value: "42".to_string(),
1218            tier: ExecutionTier::Calculator,
1219            cached: false,
1220            duration_ms: 5,
1221            stdout: None,
1222            stderr: None,
1223        };
1224
1225        let result2 = EvalResult {
1226            value: "42".to_string(),
1227            tier: ExecutionTier::Calculator,
1228            cached: false,
1229            duration_ms: 5,
1230            stdout: None,
1231            stderr: None,
1232        };
1233
1234        let result3 = EvalResult {
1235            value: "100".to_string(),
1236            tier: ExecutionTier::JustInTime,
1237            cached: true,
1238            duration_ms: 100,
1239            stdout: None,
1240            stderr: None,
1241        };
1242
1243        assert_eq!(result1, result2);
1244        assert_ne!(result1, result3);
1245    }
1246
1247    #[test]
1248    fn test_stats_collector_accessor() {
1249        let ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1250        let stats = ctx.stats_collector();
1251
1252        // Should be able to lock and access
1253        let guard = stats.lock().unwrap();
1254        let cache_stats = guard.cache_stats();
1255        assert_eq!(cache_stats.hits, 0);
1256        assert_eq!(cache_stats.misses, 0);
1257    }
1258
1259    #[test]
1260    fn test_usage_metrics_accessor() {
1261        let ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1262        let usage = ctx.usage_metrics();
1263
1264        // Should be able to lock and access
1265        let mut guard = usage.lock().unwrap();
1266        guard.record_eval();
1267        let snapshot = guard.snapshot();
1268        assert_eq!(snapshot.eval_count, 1);
1269    }
1270
1271    #[test]
1272    fn test_session_dir_stats_not_initialized() {
1273        let ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1274
1275        // Without full compilation pipeline, session_dir is None
1276        assert!(ctx.session_dir_stats().is_none());
1277    }
1278
1279    #[test]
1280    fn test_artifact_cache_stats_not_initialized() {
1281        let ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1282
1283        // Without full compilation pipeline, artifact_cache is None
1284        assert!(ctx.artifact_cache_stats().is_none());
1285    }
1286
1287    #[test]
1288    fn test_subprocess_stats_not_initialized() {
1289        let ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1290
1291        // Without full compilation pipeline, executor is None
1292        assert!(ctx.subprocess_stats().is_none());
1293    }
1294
1295    #[test]
1296    fn test_type_inference_register_and_get() {
1297        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1298
1299        // Initially no variables
1300        assert_eq!(ctx.get_variable_type("x"), None);
1301
1302        // Register a variable
1303        ctx.register_variable_type("x", "i32", false);
1304        assert_eq!(ctx.get_variable_type("x"), Some("i32".to_string()));
1305
1306        // Register mutable variable
1307        ctx.register_variable_type("y", "String", true);
1308        assert_eq!(ctx.get_variable_type("y"), Some("String".to_string()));
1309
1310        // Unknown variable still returns None
1311        assert_eq!(ctx.get_variable_type("z"), None);
1312    }
1313
1314    #[test]
1315    fn test_type_inference_get_all_variables() {
1316        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1317
1318        ctx.register_variable_type("x", "i32", false);
1319        ctx.register_variable_type("y", "String", true);
1320
1321        let vars = ctx.get_all_variable_types();
1322        assert_eq!(vars.len(), 2);
1323
1324        // Check that both variables are present (order may vary)
1325        let has_x = vars.iter().any(|(name, ty, mutable)| name == "x" && ty == "i32" && !mutable);
1326        let has_y =
1327            vars.iter().any(|(name, ty, mutable)| name == "y" && ty == "String" && *mutable);
1328        assert!(has_x);
1329        assert!(has_y);
1330    }
1331
1332    #[test]
1333    fn test_type_inference_clear() {
1334        let mut ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1335
1336        ctx.register_variable_type("x", "i32", false);
1337        ctx.register_variable_type("y", "String", true);
1338        assert_eq!(ctx.get_all_variable_types().len(), 2);
1339
1340        ctx.clear_type_information();
1341        assert_eq!(ctx.get_all_variable_types().len(), 0);
1342        assert_eq!(ctx.get_variable_type("x"), None);
1343    }
1344
1345    #[test]
1346    fn test_infer_types_from_code() {
1347        let ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1348
1349        let code = r#"
1350            fn main() {
1351                let x: i32 = 42;
1352                let mut y: String = "hello".to_string();
1353            }
1354        "#;
1355
1356        let types = ctx.infer_types(code);
1357        // The type inference should find variables in the code
1358        assert!(types.len() >= 2);
1359
1360        // Check x is found with correct type
1361        let x_info = types.iter().find(|(name, _, _)| name == "x");
1362        assert!(x_info.is_some());
1363        if let Some((_, ty, mutable)) = x_info {
1364            assert_eq!(ty, "i32");
1365            assert!(!mutable);
1366        }
1367
1368        // Check y is found with correct type
1369        let y_info = types.iter().find(|(name, _, _)| name == "y");
1370        assert!(y_info.is_some());
1371        if let Some((_, ty, mutable)) = y_info {
1372            assert_eq!(ty, "String");
1373            assert!(*mutable);
1374        }
1375    }
1376
1377    #[test]
1378    fn test_infer_types_empty_code() {
1379        let ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1380
1381        let types = ctx.infer_types("");
1382        assert!(types.is_empty());
1383    }
1384
1385    #[test]
1386    fn test_infer_types_no_variables() {
1387        let ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1388
1389        let code = "fn main() { println!(\"hello\"); }";
1390        let types = ctx.infer_types(code);
1391        assert!(types.is_empty());
1392    }
1393
1394    #[test]
1395    fn test_hash_code_consistency() {
1396        let ctx = EvalContext::new(SessionId::new("test"), ReplMode::Lisp);
1397
1398        // Same code should produce same hash
1399        let hash1 = ctx.hash_code("(+ 1 2)");
1400        let hash2 = ctx.hash_code("(+ 1 2)");
1401        assert_eq!(hash1, hash2);
1402
1403        // Different code should produce different hash
1404        let hash3 = ctx.hash_code("(+ 1 3)");
1405        assert_ne!(hash1, hash3);
1406
1407        // Hash should start with 'h' (valid Rust identifier)
1408        assert!(hash1.starts_with('h'));
1409    }
1410
1411    #[test]
1412    fn test_new_context_sexpr_mode() {
1413        let ctx = EvalContext::new(SessionId::new("test-sexpr"), ReplMode::Sexpr);
1414        assert_eq!(ctx.session_id(), &SessionId::new("test-sexpr"));
1415        assert_eq!(ctx.mode(), ReplMode::Sexpr);
1416    }
1417
1418    #[test]
1419    fn test_clone_to_preserves_mode() {
1420        let ctx = EvalContext::new(SessionId::new("session-1"), ReplMode::Sexpr);
1421        let cloned = ctx.clone_to(SessionId::new("session-2"));
1422
1423        assert_eq!(cloned.mode(), ReplMode::Sexpr);
1424    }
1425
1426    #[test]
1427    fn test_execution_tier_debug() {
1428        // Test Debug trait implementation
1429        let tier = ExecutionTier::Calculator;
1430        let debug_str = format!("{:?}", tier);
1431        assert_eq!(debug_str, "Calculator");
1432
1433        let tier = ExecutionTier::CachedLoaded;
1434        let debug_str = format!("{:?}", tier);
1435        assert_eq!(debug_str, "CachedLoaded");
1436
1437        let tier = ExecutionTier::JustInTime;
1438        let debug_str = format!("{:?}", tier);
1439        assert_eq!(debug_str, "JustInTime");
1440    }
1441
1442    #[test]
1443    fn test_execution_tier_clone() {
1444        let tier1 = ExecutionTier::Calculator;
1445        let tier2 = tier1;
1446        assert_eq!(tier1, tier2);
1447    }
1448
1449    #[test]
1450    fn test_eval_result_clone() {
1451        let result = EvalResult {
1452            value: "42".to_string(),
1453            tier: ExecutionTier::Calculator,
1454            cached: false,
1455            duration_ms: 5,
1456            stdout: Some("out".to_string()),
1457            stderr: None,
1458        };
1459
1460        let cloned = result.clone();
1461        assert_eq!(result, cloned);
1462    }
1463
1464    #[test]
1465    fn test_eval_result_debug() {
1466        let result = EvalResult {
1467            value: "42".to_string(),
1468            tier: ExecutionTier::Calculator,
1469            cached: false,
1470            duration_ms: 5,
1471            stdout: None,
1472            stderr: None,
1473        };
1474
1475        let debug_str = format!("{:?}", result);
1476        assert!(debug_str.contains("EvalResult"));
1477        assert!(debug_str.contains("42"));
1478        assert!(debug_str.contains("Calculator"));
1479    }
1480
1481    #[test]
1482    fn test_eval_error_debug() {
1483        let err = EvalError::SyntaxError { msg: "test".to_string(), pos: SourcePos::repl(1, 1, 1) };
1484
1485        let debug_str = format!("{:?}", err);
1486        assert!(debug_str.contains("SyntaxError"));
1487    }
1488}