runmat_repl/
lib.rs

1use anyhow::Result;
2use log::{debug, info, warn};
3use runmat_builtins::Value;
4use runmat_gc::{gc_configure, gc_stats, GcConfig};
5
6use runmat_lexer::{tokenize, tokenize_detailed, Token as LexToken};
7use runmat_parser::parse;
8use runmat_snapshot::{Snapshot, SnapshotConfig, SnapshotLoader};
9use runmat_turbine::TurbineEngine;
10use std::collections::HashMap;
11use std::path::Path;
12use std::sync::Arc;
13use std::time::Instant;
14
15/// Enhanced REPL execution engine that integrates all RunMat components
16pub struct ReplEngine {
17    /// JIT compiler engine (optional for fallback mode)
18    jit_engine: Option<TurbineEngine>,
19    /// Verbose output for debugging
20    verbose: bool,
21    /// Execution statistics
22    stats: ExecutionStats,
23    /// Persistent variable context for REPL sessions
24    variables: HashMap<String, Value>,
25    /// Current variable array for bytecode execution
26    variable_array: Vec<Value>,
27    /// Mapping from variable names to VarId indices
28    variable_names: HashMap<String, usize>,
29    /// User-defined functions context for REPL sessions
30    function_definitions: HashMap<String, runmat_hir::HirStmt>,
31    /// Loaded snapshot for standard library preloading
32    snapshot: Option<Arc<Snapshot>>,
33}
34
35#[derive(Debug, Default)]
36pub struct ExecutionStats {
37    pub total_executions: usize,
38    pub jit_compiled: usize,
39    pub interpreter_fallback: usize,
40    pub total_execution_time_ms: u64,
41    pub average_execution_time_ms: f64,
42}
43
44#[derive(Debug)]
45pub struct ExecutionResult {
46    pub value: Option<Value>,
47    pub execution_time_ms: u64,
48    pub used_jit: bool,
49    pub error: Option<String>,
50    /// Type information displayed when output is suppressed by semicolon
51    pub type_info: Option<String>,
52}
53
54/// Format value type information like MATLAB (e.g., "1000x1 vector", "3x3 matrix")
55fn format_type_info(value: &Value) -> String {
56    match value {
57        Value::Int(_) => "scalar".to_string(),
58        Value::Num(_) => "scalar".to_string(),
59        Value::Bool(_) => "logical scalar".to_string(),
60        Value::String(_) => "string".to_string(),
61        Value::StringArray(sa) => {
62            // MATLAB displays string arrays as m x n string array; for test's purpose, we classify scalar string arrays as "string"
63            if sa.shape == vec![1, 1] {
64                "string".to_string()
65            } else {
66                format!("{}x{} string array", sa.rows(), sa.cols())
67            }
68        }
69        Value::CharArray(ca) => {
70            if ca.rows == 1 && ca.cols == 1 {
71                "char".to_string()
72            } else {
73                format!("{}x{} char array", ca.rows, ca.cols)
74            }
75        }
76        Value::Tensor(m) => {
77            if m.rows() == 1 && m.cols() == 1 {
78                "scalar".to_string()
79            } else if m.rows() == 1 || m.cols() == 1 {
80                format!("{}x{} vector", m.rows(), m.cols())
81            } else {
82                format!("{}x{} matrix", m.rows(), m.cols())
83            }
84        }
85        Value::Cell(cells) => {
86            if cells.data.len() == 1 {
87                "1x1 cell".to_string()
88            } else {
89                format!("{}x1 cell array", cells.data.len())
90            }
91        }
92        Value::GpuTensor(h) => {
93            if h.shape.len() == 2 {
94                let r = h.shape[0];
95                let c = h.shape[1];
96                if r == 1 && c == 1 {
97                    "scalar (gpu)".to_string()
98                } else if r == 1 || c == 1 {
99                    format!("{r}x{c} vector (gpu)")
100                } else {
101                    format!("{r}x{c} matrix (gpu)")
102                }
103            } else {
104                format!("Tensor{:?} (gpu)", h.shape)
105            }
106        }
107        _ => "value".to_string(),
108    }
109}
110
111impl ReplEngine {
112    /// Create a new REPL engine
113    pub fn new() -> Result<Self> {
114        Self::with_options(true, false) // JIT enabled, verbose disabled
115    }
116
117    /// Create a new REPL engine with specific options
118    pub fn with_options(enable_jit: bool, verbose: bool) -> Result<Self> {
119        Self::with_snapshot(enable_jit, verbose, None::<&str>)
120    }
121
122    /// Create a new REPL engine with snapshot loading
123    pub fn with_snapshot<P: AsRef<Path>>(
124        enable_jit: bool,
125        verbose: bool,
126        snapshot_path: Option<P>,
127    ) -> Result<Self> {
128        // Load snapshot if provided
129        let snapshot = if let Some(path) = snapshot_path {
130            match Self::load_snapshot(path.as_ref()) {
131                Ok(snapshot) => {
132                    info!(
133                        "Snapshot loaded successfully from {}",
134                        path.as_ref().display()
135                    );
136                    Some(Arc::new(snapshot))
137                }
138                Err(e) => {
139                    warn!(
140                        "Failed to load snapshot from {}: {}, continuing without snapshot",
141                        path.as_ref().display(),
142                        e
143                    );
144                    None
145                }
146            }
147        } else {
148            None
149        };
150
151        let jit_engine = if enable_jit {
152            match TurbineEngine::new() {
153                Ok(engine) => {
154                    info!("JIT compiler initialized successfully");
155                    Some(engine)
156                }
157                Err(e) => {
158                    warn!("JIT compiler initialization failed: {e}, falling back to interpreter");
159                    None
160                }
161            }
162        } else {
163            info!("JIT compiler disabled, using interpreter only");
164            None
165        };
166
167        Ok(Self {
168            jit_engine,
169            verbose,
170            stats: ExecutionStats::default(),
171            variables: HashMap::new(),
172            variable_array: Vec::new(),
173            variable_names: HashMap::new(),
174            function_definitions: HashMap::new(),
175            snapshot,
176        })
177    }
178
179    /// Load a snapshot from disk
180    fn load_snapshot(path: &Path) -> Result<Snapshot> {
181        let mut loader = SnapshotLoader::new(SnapshotConfig::default());
182        let (snapshot, _stats) = loader
183            .load(path)
184            .map_err(|e| anyhow::anyhow!("Failed to load snapshot: {}", e))?;
185        Ok(snapshot)
186    }
187
188    /// Get snapshot information
189    pub fn snapshot_info(&self) -> Option<String> {
190        self.snapshot.as_ref().map(|snapshot| {
191            format!(
192                "Snapshot loaded: {} builtins, {} HIR functions, {} bytecode entries",
193                snapshot.builtins.functions.len(),
194                snapshot.hir_cache.functions.len(),
195                snapshot.bytecode_cache.stdlib_bytecode.len()
196            )
197        })
198    }
199
200    /// Check if a snapshot is loaded
201    pub fn has_snapshot(&self) -> bool {
202        self.snapshot.is_some()
203    }
204
205    /// Execute MATLAB/Octave code
206    pub fn execute(&mut self, input: &str) -> Result<ExecutionResult> {
207        let start_time = Instant::now();
208        self.stats.total_executions += 1;
209
210        if self.verbose {
211            debug!("Executing: {}", input.trim());
212        }
213
214        // Parse the input
215        let ast = parse(input)
216            .map_err(|e| anyhow::anyhow!("Failed to parse input '{}': {}", input, e))?;
217        if self.verbose {
218            debug!("AST: {ast:?}");
219        }
220
221        // Lower to HIR with existing variable and function context
222        let lowering_result = runmat_hir::lower_with_full_context(
223            &ast,
224            &self.variable_names,
225            &self.function_definitions,
226        )
227        .map_err(|e| anyhow::anyhow!("Failed to lower to HIR: {}", e))?;
228        let (hir, updated_vars, updated_functions) = (
229            lowering_result.hir,
230            lowering_result.variables,
231            lowering_result.functions,
232        );
233        if self.verbose {
234            debug!("HIR generated successfully");
235        }
236
237        // Compile to bytecode with existing function definitions
238        let existing_functions = self.convert_hir_functions_to_user_functions();
239        let bytecode = runmat_ignition::compile_with_functions(&hir, &existing_functions)
240            .map_err(|e| anyhow::anyhow!("Failed to compile to bytecode: {}", e))?;
241        if self.verbose {
242            debug!(
243                "Bytecode compiled: {} instructions",
244                bytecode.instructions.len()
245            );
246        }
247
248        // Prepare variable array with existing values before execution
249        self.prepare_variable_array_for_execution(&bytecode, &updated_vars);
250
251        if self.verbose {
252            debug!(
253                "Variable array after preparation: {:?}",
254                self.variable_array
255            );
256            debug!("Updated variable mapping: {updated_vars:?}");
257            debug!("Bytecode instructions: {:?}", bytecode.instructions);
258        }
259
260        let mut used_jit = false;
261        let mut result_value: Option<Value> = None; // Always start fresh for each execution
262        let mut suppressed_value: Option<Value> = None; // Track value for type info when suppressed
263        let mut error = None;
264
265        // Check if this is an expression statement (ends with Pop)
266        let is_expression_stmt = bytecode
267            .instructions
268            .last()
269            .map(|instr| matches!(instr, runmat_ignition::Instr::Pop))
270            .unwrap_or(false);
271
272        // Detect whether the user's input ends with a semicolon at the token level
273        let ends_with_semicolon = {
274            let toks = tokenize_detailed(input);
275            toks.into_iter()
276                .rev()
277                .map(|t| t.token)
278                .find(|_| true)
279                .map(|t| matches!(t, LexToken::Semicolon))
280                .unwrap_or(false)
281        };
282
283        // Check if this is a semicolon-suppressed statement (expression or assignment)
284        // Control flow statements never return values regardless of semicolons
285        let is_semicolon_suppressed = if hir.body.len() == 1 {
286            match &hir.body[0] {
287                runmat_hir::HirStmt::ExprStmt(_, _) => ends_with_semicolon,
288                runmat_hir::HirStmt::Assign(_, _, _) => ends_with_semicolon,
289                runmat_hir::HirStmt::If { .. }
290                | runmat_hir::HirStmt::While { .. }
291                | runmat_hir::HirStmt::For { .. }
292                | runmat_hir::HirStmt::Break
293                | runmat_hir::HirStmt::Continue
294                | runmat_hir::HirStmt::Return
295                | runmat_hir::HirStmt::Function { .. }
296                | runmat_hir::HirStmt::MultiAssign(_, _, _)
297                | runmat_hir::HirStmt::AssignLValue(_, _, _)
298                | runmat_hir::HirStmt::Switch { .. }
299                | runmat_hir::HirStmt::TryCatch { .. }
300                | runmat_hir::HirStmt::Global(_)
301                | runmat_hir::HirStmt::Persistent(_)
302                | runmat_hir::HirStmt::Import {
303                    path: _,
304                    wildcard: _,
305                }
306                | runmat_hir::HirStmt::ClassDef { .. } => true,
307            }
308        } else {
309            false
310        };
311
312        if self.verbose {
313            debug!("HIR body len: {}", hir.body.len());
314            if !hir.body.is_empty() {
315                debug!("HIR statement: {:?}", &hir.body[0]);
316            }
317            debug!("is_semicolon_suppressed: {is_semicolon_suppressed}");
318        }
319
320        // Use JIT for assignments, interpreter for expressions (to capture results properly)
321        if let Some(ref mut jit_engine) = &mut self.jit_engine {
322            if !is_expression_stmt {
323                // Ensure variable array is large enough
324                if self.variable_array.len() < bytecode.var_count {
325                    self.variable_array
326                        .resize(bytecode.var_count, Value::Num(0.0));
327                }
328
329                if self.verbose {
330                    debug!(
331                        "JIT path for assignment: variable_array size: {}, bytecode.var_count: {}",
332                        self.variable_array.len(),
333                        bytecode.var_count
334                    );
335                }
336
337                // Use JIT for assignments
338                match jit_engine.execute_or_compile(&bytecode, &mut self.variable_array) {
339                    Ok((_, actual_used_jit)) => {
340                        used_jit = actual_used_jit;
341                        if actual_used_jit {
342                            self.stats.jit_compiled += 1;
343                        } else {
344                            self.stats.interpreter_fallback += 1;
345                        }
346                        // For assignments, capture the assigned value for both display and type info
347                        // Prefer the variable slot indicated by HIR if available.
348                        let assignment_value =
349                            if let Some(runmat_hir::HirStmt::Assign(var_id, _, _)) =
350                                hir.body.first()
351                            {
352                                if var_id.0 < self.variable_array.len() {
353                                    Some(self.variable_array[var_id.0].clone())
354                                } else {
355                                    None
356                                }
357                            } else {
358                                self.variable_array
359                                    .iter()
360                                    .rev()
361                                    .find(|v| !matches!(v, Value::Num(0.0)))
362                                    .cloned()
363                            };
364
365                        if !is_semicolon_suppressed {
366                            result_value = assignment_value.clone();
367                            if self.verbose {
368                                debug!("JIT assignment result: {result_value:?}");
369                            }
370                        } else {
371                            suppressed_value = assignment_value;
372                            if self.verbose {
373                                debug!("JIT assignment suppressed due to semicolon, captured for type info");
374                            }
375                        }
376
377                        if self.verbose {
378                            debug!(
379                                "{} assignment successful, variable_array: {:?}",
380                                if actual_used_jit {
381                                    "JIT"
382                                } else {
383                                    "Interpreter"
384                                },
385                                self.variable_array
386                            );
387                        }
388                    }
389                    Err(e) => {
390                        if self.verbose {
391                            debug!("JIT execution failed: {e}, using interpreter");
392                        }
393                        // Fall back to interpreter
394                    }
395                }
396            }
397        }
398
399        // Use interpreter if JIT failed or is disabled
400        if !used_jit {
401            if self.verbose {
402                debug!(
403                    "Interpreter path: variable_array size: {}, bytecode.var_count: {}",
404                    self.variable_array.len(),
405                    bytecode.var_count
406                );
407            }
408
409            // For expressions, modify bytecode to store result in a temp variable instead of using stack
410            let mut execution_bytecode = bytecode.clone();
411            if is_expression_stmt && !execution_bytecode.instructions.is_empty() {
412                execution_bytecode.instructions.pop(); // Remove the Pop instruction
413
414                // Add StoreVar instruction to store the result in a temporary variable
415                let temp_var_id = execution_bytecode.var_count;
416                execution_bytecode
417                    .instructions
418                    .push(runmat_ignition::Instr::StoreVar(temp_var_id));
419                execution_bytecode.var_count += 1; // Expand variable count for temp variable
420
421                // Ensure our variable array can hold the temporary variable
422                if self.variable_array.len() <= temp_var_id {
423                    self.variable_array.resize(temp_var_id + 1, Value::Num(0.0));
424                }
425
426                if self.verbose {
427                    debug!(
428                        "Modified expression bytecode, new instructions: {:?}",
429                        execution_bytecode.instructions
430                    );
431                }
432            }
433
434            match self.interpret_with_context(&execution_bytecode) {
435                Ok(results) => {
436                    // Only increment interpreter_fallback if JIT wasn't attempted
437                    if self.jit_engine.is_none() || is_expression_stmt {
438                        self.stats.interpreter_fallback += 1;
439                    }
440                    if self.verbose {
441                        debug!("Interpreter results: {results:?}");
442                    }
443
444                    // Handle assignment statements (x = 42 should show the assigned value unless suppressed)
445                    if hir.body.len() == 1 {
446                        if let runmat_hir::HirStmt::Assign(var_id, _, _) = &hir.body[0] {
447                            if self.verbose {
448                                debug!(
449                                    "Assignment detected, var_id: {}, ends_with_semicolon: {}",
450                                    var_id.0, ends_with_semicolon
451                                );
452                            }
453                            // For assignments, capture the assigned value for both display and type info
454                            if var_id.0 < self.variable_array.len() {
455                                let assignment_value = self.variable_array[var_id.0].clone();
456                                if !is_semicolon_suppressed {
457                                    result_value = Some(assignment_value);
458                                    if self.verbose {
459                                        debug!("Setting assignment result_value: {result_value:?}");
460                                    }
461                                } else {
462                                    suppressed_value = Some(assignment_value);
463                                    if self.verbose {
464                                        debug!("Assignment suppressed, captured for type info: {suppressed_value:?}");
465                                    }
466                                }
467                            }
468                        }
469                    }
470
471                    // For expressions, get the result from the temporary variable (capture for both display and type info)
472                    if is_expression_stmt
473                        && !execution_bytecode.instructions.is_empty()
474                        && result_value.is_none()
475                        && suppressed_value.is_none()
476                    {
477                        let temp_var_id = execution_bytecode.var_count - 1; // The temp variable we added
478                        if temp_var_id < self.variable_array.len() {
479                            let expression_value = self.variable_array[temp_var_id].clone();
480                            if !is_semicolon_suppressed {
481                                result_value = Some(expression_value);
482                                if self.verbose {
483                                    debug!("Expression result from temp var {temp_var_id}: {result_value:?}");
484                                }
485                            } else {
486                                suppressed_value = Some(expression_value);
487                                if self.verbose {
488                                    debug!("Expression suppressed, captured for type info from temp var {temp_var_id}: {suppressed_value:?}");
489                                }
490                            }
491                        }
492                    } else if !is_semicolon_suppressed && result_value.is_none() {
493                        result_value = results.into_iter().last();
494                        if self.verbose {
495                            debug!("Fallback result from interpreter: {result_value:?}");
496                        }
497                    }
498
499                    if self.verbose {
500                        debug!("Final result_value: {result_value:?}");
501                    }
502                    debug!(
503                        "Interpreter execution successful, variable_array: {:?}",
504                        self.variable_array
505                    );
506                }
507                Err(e) => {
508                    debug!("Interpreter execution failed: {e}");
509                    error = Some(format!("Execution failed: {e}"));
510                }
511            }
512        }
513
514        let execution_time = start_time.elapsed();
515        let execution_time_ms = execution_time.as_millis() as u64;
516
517        self.stats.total_execution_time_ms += execution_time_ms;
518        self.stats.average_execution_time_ms =
519            self.stats.total_execution_time_ms as f64 / self.stats.total_executions as f64;
520
521        // Update variable names mapping and function definitions if execution was successful
522        if error.is_none() {
523            self.variable_names = updated_vars;
524            self.function_definitions = updated_functions;
525        }
526
527        if self.verbose {
528            debug!("Execution completed in {execution_time_ms}ms (JIT: {used_jit})");
529        }
530
531        // Generate type info if we have a suppressed value
532        let type_info = suppressed_value.as_ref().map(format_type_info);
533
534        // Final fallback: if not suppressed and still no value, try last non-zero variable slot
535        if !is_semicolon_suppressed && result_value.is_none() {
536            if let Some(v) = self
537                .variable_array
538                .iter()
539                .rev()
540                .find(|v| !matches!(v, Value::Num(0.0)))
541                .cloned()
542            {
543                result_value = Some(v);
544            }
545        }
546
547        Ok(ExecutionResult {
548            value: result_value,
549            execution_time_ms,
550            used_jit,
551            error,
552            type_info,
553        })
554    }
555
556    /// Get execution statistics
557    pub fn stats(&self) -> &ExecutionStats {
558        &self.stats
559    }
560
561    /// Reset execution statistics
562    pub fn reset_stats(&mut self) {
563        self.stats = ExecutionStats::default();
564    }
565
566    /// Clear all variables in the REPL context
567    pub fn clear_variables(&mut self) {
568        self.variables.clear();
569        self.variable_array.clear();
570        self.variable_names.clear();
571    }
572
573    /// Get a copy of current variables
574    pub fn get_variables(&self) -> &HashMap<String, Value> {
575        &self.variables
576    }
577
578    /// Interpret bytecode with persistent variable context
579    fn interpret_with_context(
580        &mut self,
581        bytecode: &runmat_ignition::Bytecode,
582    ) -> Result<Vec<Value>, String> {
583        // Variable array should already be prepared by prepare_variable_array_for_execution
584
585        // Use the main Ignition interpreter which has full function and scoping support
586        match runmat_ignition::interpret_with_vars(
587            bytecode,
588            &mut self.variable_array,
589            Some("<repl>"),
590        ) {
591            Ok(result) => {
592                // Update the variables HashMap for display purposes
593                self.variables.clear();
594                for (i, value) in self.variable_array.iter().enumerate() {
595                    if !matches!(value, Value::Num(0.0)) {
596                        // Only store non-zero values to avoid clutter
597                        self.variables.insert(format!("var_{i}"), value.clone());
598                    }
599                }
600
601                Ok(result)
602            }
603            Err(e) => Err(e),
604        }
605    }
606
607    /// Prepare variable array for execution by populating with existing values
608    fn prepare_variable_array_for_execution(
609        &mut self,
610        bytecode: &runmat_ignition::Bytecode,
611        updated_var_mapping: &HashMap<String, usize>,
612    ) {
613        // Create a new variable array of the correct size
614        let mut new_variable_array = vec![Value::Num(0.0); bytecode.var_count];
615
616        // Populate with existing values based on the variable mapping
617        for (var_name, &new_var_id) in updated_var_mapping {
618            if let Some(&old_var_id) = self.variable_names.get(var_name) {
619                // If we had this variable before, copy its value to the new position
620                if old_var_id < self.variable_array.len() && new_var_id < new_variable_array.len() {
621                    new_variable_array[new_var_id] = self.variable_array[old_var_id].clone();
622                }
623            }
624        }
625
626        // Update our variable array and mapping
627        self.variable_array = new_variable_array;
628    }
629
630    /// Convert stored HIR function definitions to UserFunction format for compilation
631    fn convert_hir_functions_to_user_functions(
632        &self,
633    ) -> HashMap<String, runmat_ignition::UserFunction> {
634        let mut user_functions = HashMap::new();
635
636        for (name, hir_stmt) in &self.function_definitions {
637            if let runmat_hir::HirStmt::Function {
638                name: func_name,
639                params,
640                outputs,
641                body,
642                has_varargin: _,
643                has_varargout: _,
644            } = hir_stmt
645            {
646                // Use the existing HIR utilities to calculate variable count
647                let var_map =
648                    runmat_hir::remapping::create_complete_function_var_map(params, outputs, body);
649                let max_local_var = var_map.len();
650
651                let user_func = runmat_ignition::UserFunction {
652                    name: func_name.clone(),
653                    params: params.clone(),
654                    outputs: outputs.clone(),
655                    body: body.clone(),
656                    local_var_count: max_local_var,
657                    has_varargin: false,
658                    has_varargout: false,
659                };
660                user_functions.insert(name.clone(), user_func);
661            }
662        }
663
664        user_functions
665    }
666
667    /// Configure garbage collector
668    pub fn configure_gc(&self, config: GcConfig) -> Result<()> {
669        gc_configure(config)
670            .map_err(|e| anyhow::anyhow!("Failed to configure garbage collector: {}", e))
671    }
672
673    /// Get GC statistics
674    pub fn gc_stats(&self) -> runmat_gc::GcStats {
675        gc_stats()
676    }
677
678    /// Show detailed system information
679    pub fn show_system_info(&self) {
680        println!("RunMat REPL Engine Status");
681        println!("==========================");
682        println!();
683
684        println!(
685            "JIT Compiler: {}",
686            if self.jit_engine.is_some() {
687                "Available"
688            } else {
689                "Disabled/Failed"
690            }
691        );
692        println!("Verbose Mode: {}", self.verbose);
693        println!();
694
695        println!("Execution Statistics:");
696        println!("  Total Executions: {}", self.stats.total_executions);
697        println!("  JIT Compiled: {}", self.stats.jit_compiled);
698        println!("  Interpreter Used: {}", self.stats.interpreter_fallback);
699        println!(
700            "  Average Time: {:.2}ms",
701            self.stats.average_execution_time_ms
702        );
703        println!();
704
705        let gc_stats = self.gc_stats();
706        println!("Garbage Collector:");
707        println!(
708            "  Total Allocations: {}",
709            gc_stats
710                .total_allocations
711                .load(std::sync::atomic::Ordering::Relaxed)
712        );
713        println!(
714            "  Minor Collections: {}",
715            gc_stats
716                .minor_collections
717                .load(std::sync::atomic::Ordering::Relaxed)
718        );
719        println!(
720            "  Major Collections: {}",
721            gc_stats
722                .major_collections
723                .load(std::sync::atomic::Ordering::Relaxed)
724        );
725        println!(
726            "  Current Memory: {:.2} MB",
727            gc_stats
728                .current_memory_usage
729                .load(std::sync::atomic::Ordering::Relaxed) as f64
730                / 1024.0
731                / 1024.0
732        );
733        println!();
734    }
735}
736
737impl Default for ReplEngine {
738    fn default() -> Self {
739        Self::new().expect("Failed to create default REPL engine")
740    }
741}
742
743/// Tokenize the input string and return a space separated string of token names.
744/// This is kept for backward compatibility with existing tests.
745pub fn format_tokens(input: &str) -> String {
746    let tokens = tokenize(input);
747    tokens
748        .into_iter()
749        .map(|t| format!("{t:?}"))
750        .collect::<Vec<_>>()
751        .join(" ")
752}
753
754/// Execute MATLAB/Octave code and return the result as a formatted string
755pub fn execute_and_format(input: &str) -> String {
756    match ReplEngine::new() {
757        Ok(mut engine) => match engine.execute(input) {
758            Ok(result) => {
759                if let Some(error) = result.error {
760                    format!("Error: {error}")
761                } else if let Some(value) = result.value {
762                    format!("{value:?}")
763                } else {
764                    "".to_string()
765                }
766            }
767            Err(e) => format!("Error: {e}"),
768        },
769        Err(e) => format!("Engine Error: {e}"),
770    }
771}