Skip to main content

runmat_core/
lib.rs

1#![allow(clippy::result_large_err)]
2
3use anyhow::Result;
4use runmat_builtins::{self, Tensor, Type, Value};
5use runmat_gc::{gc_configure, gc_stats, GcConfig};
6use tracing::{debug, info, info_span, warn};
7
8#[cfg(not(target_arch = "wasm32"))]
9use runmat_accelerate_api::provider as accel_provider;
10use runmat_accelerate_api::{provider_for_handle, ProviderPrecision};
11use runmat_hir::{LoweringContext, LoweringResult, SemanticError, SourceId};
12use runmat_ignition::CompileError;
13use runmat_lexer::{tokenize_detailed, Token as LexToken};
14pub use runmat_parser::CompatMode;
15use runmat_parser::{parse_with_options, ParserOptions, SyntaxError};
16use runmat_runtime::warning_store::RuntimeWarning;
17use runmat_runtime::{build_runtime_error, gather_if_needed_async, RuntimeError};
18use runmat_runtime::{
19    runtime_export_workspace_state, runtime_import_workspace_state, WorkspaceReplayMode,
20};
21#[cfg(target_arch = "wasm32")]
22use runmat_snapshot::SnapshotBuilder;
23use runmat_snapshot::{Snapshot, SnapshotConfig, SnapshotLoader};
24use runmat_time::Instant;
25#[cfg(feature = "jit")]
26use runmat_turbine::TurbineEngine;
27use std::collections::{HashMap, HashSet};
28use std::future::Future;
29#[cfg(not(target_arch = "wasm32"))]
30use std::path::Path;
31use std::pin::Pin;
32use std::sync::{
33    atomic::{AtomicBool, Ordering},
34    Arc, Mutex,
35};
36use uuid::Uuid;
37
38#[cfg(all(test, target_arch = "wasm32"))]
39wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
40
41mod fusion_snapshot;
42mod value_metadata;
43use fusion_snapshot::build_fusion_snapshot;
44
45mod telemetry;
46pub use telemetry::{
47    TelemetryFailureInfo, TelemetryHost, TelemetryPlatformInfo, TelemetryRunConfig,
48    TelemetryRunFinish, TelemetryRunGuard, TelemetrySink,
49};
50
51pub use value_metadata::{
52    approximate_size_bytes, matlab_class_name, numeric_dtype_label, preview_numeric_values,
53    value_shape,
54};
55
56/// Host-agnostic RunMat execution session (parser + interpreter + optional JIT).
57pub struct RunMatSession {
58    /// JIT compiler engine (optional for fallback mode)
59    #[cfg(feature = "jit")]
60    jit_engine: Option<TurbineEngine>,
61    /// Verbose output for debugging
62    verbose: bool,
63    /// Execution statistics
64    stats: ExecutionStats,
65    /// Persistent variable context for session state
66    variables: HashMap<String, Value>,
67    /// Current variable array for bytecode execution
68    variable_array: Vec<Value>,
69    /// Mapping from variable names to VarId indices
70    variable_names: HashMap<String, usize>,
71    /// Persistent workspace values keyed by variable name
72    workspace_values: HashMap<String, Value>,
73    /// User-defined functions context for session state
74    function_definitions: HashMap<String, runmat_hir::HirStmt>,
75    /// Interned source pool for user-defined functions
76    source_pool: SourcePool,
77    /// Source IDs for user-defined functions keyed by name
78    function_source_ids: HashMap<String, SourceId>,
79    /// Loaded snapshot for standard library preloading
80    snapshot: Option<Arc<Snapshot>>,
81    /// Cooperative cancellation flag shared with the runtime.
82    interrupt_flag: Arc<AtomicBool>,
83    /// Tracks whether an execution is currently active.
84    is_executing: bool,
85    /// Optional async input handler (Phase 2). When set, stdin interactions are awaited
86    /// internally by `ExecuteFuture` rather than being surfaced as "pending requests".
87    async_input_handler: Option<SharedAsyncInputHandler>,
88    /// Maximum number of call stack frames to retain for diagnostics.
89    callstack_limit: usize,
90    /// Namespace prefix for runtime/semantic error identifiers.
91    error_namespace: String,
92    /// Default source name used for diagnostics.
93    default_source_name: String,
94    /// Override source name for the current execution.
95    source_name_override: Option<String>,
96    telemetry_consent: bool,
97    telemetry_client_id: Option<String>,
98    telemetry_platform: TelemetryPlatformInfo,
99    telemetry_sink: Option<Arc<dyn TelemetrySink>>,
100    workspace_preview_tokens: HashMap<Uuid, WorkspaceMaterializeTicket>,
101    workspace_version: u64,
102    emit_fusion_plan: bool,
103    compat_mode: CompatMode,
104}
105
106#[derive(Debug, Clone)]
107struct SourceText {
108    name: Arc<str>,
109    text: Arc<str>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Hash)]
113struct SourceKey {
114    name: Arc<str>,
115    text: Arc<str>,
116}
117
118#[derive(Default)]
119struct SourcePool {
120    sources: Vec<SourceText>,
121    index: HashMap<SourceKey, SourceId>,
122}
123
124impl SourcePool {
125    fn intern(&mut self, name: &str, text: &str) -> SourceId {
126        let name: Arc<str> = Arc::from(name);
127        let text: Arc<str> = Arc::from(text);
128        let key = SourceKey {
129            name: Arc::clone(&name),
130            text: Arc::clone(&text),
131        };
132        if let Some(id) = self.index.get(&key) {
133            return *id;
134        }
135        let id = SourceId(self.sources.len());
136        self.sources.push(SourceText { name, text });
137        self.index.insert(key, id);
138        id
139    }
140
141    fn get(&self, id: SourceId) -> Option<&SourceText> {
142        self.sources.get(id.0)
143    }
144}
145
146fn line_col_from_offset(source: &str, offset: usize) -> (usize, usize) {
147    let mut line = 1;
148    let mut line_start = 0;
149    for (idx, ch) in source.char_indices() {
150        if idx >= offset {
151            break;
152        }
153        if ch == '\n' {
154            line += 1;
155            line_start = idx + 1;
156        }
157    }
158    let col = offset.saturating_sub(line_start) + 1;
159    (line, col)
160}
161
162#[derive(Debug)]
163pub enum RunError {
164    Syntax(SyntaxError),
165    Semantic(SemanticError),
166    Compile(CompileError),
167    Runtime(RuntimeError),
168}
169
170impl std::fmt::Display for RunError {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            RunError::Syntax(err) => write!(f, "{err}"),
174            RunError::Semantic(err) => write!(f, "{err}"),
175            RunError::Compile(err) => write!(f, "{err}"),
176            RunError::Runtime(err) => write!(f, "{err}"),
177        }
178    }
179}
180
181impl std::error::Error for RunError {}
182
183impl From<SyntaxError> for RunError {
184    fn from(value: SyntaxError) -> Self {
185        RunError::Syntax(value)
186    }
187}
188
189impl From<SemanticError> for RunError {
190    fn from(value: SemanticError) -> Self {
191        RunError::Semantic(value)
192    }
193}
194
195impl From<CompileError> for RunError {
196    fn from(value: CompileError) -> Self {
197        RunError::Compile(value)
198    }
199}
200
201impl From<RuntimeError> for RunError {
202    fn from(value: RuntimeError) -> Self {
203        RunError::Runtime(value)
204    }
205}
206
207impl RunError {
208    pub fn telemetry_failure_info(&self) -> TelemetryFailureInfo {
209        match self {
210            RunError::Syntax(_err) => TelemetryFailureInfo {
211                stage: "parser".to_string(),
212                code: "RunMat:ParserError".to_string(),
213                has_span: true,
214                component: Some("unknown".to_string()),
215            },
216            RunError::Semantic(err) => TelemetryFailureInfo {
217                stage: "hir".to_string(),
218                code: err
219                    .identifier
220                    .clone()
221                    .unwrap_or_else(|| "RunMat:SemanticError".to_string()),
222                has_span: err.span.is_some(),
223                component: telemetry_component_for_identifier(err.identifier.as_deref()),
224            },
225            RunError::Compile(err) => TelemetryFailureInfo {
226                stage: "compile".to_string(),
227                code: err
228                    .identifier
229                    .clone()
230                    .unwrap_or_else(|| "RunMat:CompileError".to_string()),
231                has_span: err.span.is_some(),
232                component: telemetry_component_for_identifier(err.identifier.as_deref()),
233            },
234            RunError::Runtime(err) => runtime_error_telemetry_failure_info(err),
235        }
236    }
237}
238
239pub fn runtime_error_telemetry_failure_info(err: &RuntimeError) -> TelemetryFailureInfo {
240    let identifier = err
241        .identifier()
242        .map(|value| value.to_string())
243        .unwrap_or_else(|| "RunMat:RuntimeError".to_string());
244    TelemetryFailureInfo {
245        stage: "runtime".to_string(),
246        code: identifier.clone(),
247        has_span: err.span.is_some(),
248        component: telemetry_component_for_identifier(Some(identifier.as_str())),
249    }
250}
251
252fn telemetry_component_for_identifier(identifier: Option<&str>) -> Option<String> {
253    let lower = identifier?.to_ascii_lowercase();
254    if lower.contains("undefined") || lower.contains("name") || lower.contains("import") {
255        return Some("name_resolution".to_string());
256    }
257    if lower.contains("type") || lower.contains("dimension") || lower.contains("bounds") {
258        return Some("typecheck".to_string());
259    }
260    if lower.contains("cancel") || lower.contains("interrupt") {
261        return Some("cancellation".to_string());
262    }
263    if lower.contains("io") || lower.contains("filesystem") {
264        return Some("io".to_string());
265    }
266    if lower.contains("network") || lower.contains("timeout") {
267        return Some("network".to_string());
268    }
269    if lower.contains("internal") || lower.contains("panic") {
270        return Some("internal".to_string());
271    }
272    None
273}
274
275struct PreparedExecution {
276    ast: runmat_parser::Program,
277    lowering: LoweringResult,
278    bytecode: runmat_ignition::Bytecode,
279}
280
281#[derive(Debug, Default)]
282pub struct ExecutionStats {
283    pub total_executions: usize,
284    pub jit_compiled: usize,
285    pub interpreter_fallback: usize,
286    pub total_execution_time_ms: u64,
287    pub average_execution_time_ms: f64,
288}
289
290#[derive(Debug, Clone)]
291pub enum StdinEventKind {
292    Line,
293    KeyPress,
294}
295
296#[derive(Debug, Clone)]
297pub struct StdinEvent {
298    pub prompt: String,
299    pub kind: StdinEventKind,
300    pub echo: bool,
301    pub value: Option<String>,
302    pub error: Option<String>,
303}
304
305#[derive(Debug, Clone)]
306pub enum InputRequestKind {
307    Line { echo: bool },
308    KeyPress,
309}
310
311#[derive(Debug, Clone)]
312pub struct InputRequest {
313    pub prompt: String,
314    pub kind: InputRequestKind,
315}
316
317#[derive(Debug, Clone)]
318pub enum InputResponse {
319    Line(String),
320    KeyPress,
321}
322
323type SharedAsyncInputHandler = Arc<
324    dyn Fn(InputRequest) -> Pin<Box<dyn Future<Output = Result<InputResponse, String>> + 'static>>
325        + Send
326        + Sync,
327>;
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub enum ExecutionStreamKind {
331    Stdout,
332    Stderr,
333    ClearScreen,
334}
335
336#[derive(Debug, Clone)]
337pub struct ExecutionStreamEntry {
338    pub stream: ExecutionStreamKind,
339    pub text: String,
340    pub timestamp_ms: u64,
341}
342
343#[derive(Debug, Clone)]
344pub struct WorkspacePreview {
345    pub values: Vec<f64>,
346    pub truncated: bool,
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub enum WorkspaceResidency {
351    Cpu,
352    Gpu,
353    Unknown,
354}
355
356impl WorkspaceResidency {
357    pub fn as_str(&self) -> &'static str {
358        match self {
359            WorkspaceResidency::Cpu => "cpu",
360            WorkspaceResidency::Gpu => "gpu",
361            WorkspaceResidency::Unknown => "unknown",
362        }
363    }
364}
365
366#[derive(Debug, Clone)]
367pub struct WorkspaceEntry {
368    pub name: String,
369    pub class_name: String,
370    pub dtype: Option<String>,
371    pub shape: Vec<usize>,
372    pub is_gpu: bool,
373    pub size_bytes: Option<u64>,
374    pub preview: Option<WorkspacePreview>,
375    pub residency: WorkspaceResidency,
376    pub preview_token: Option<Uuid>,
377}
378
379#[derive(Debug, Clone)]
380pub struct WorkspaceSnapshot {
381    pub full: bool,
382    pub version: u64,
383    pub values: Vec<WorkspaceEntry>,
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub enum WorkspaceExportMode {
388    Off,
389    Auto,
390    Force,
391}
392
393#[derive(Debug, Clone)]
394pub struct MaterializedVariable {
395    pub name: String,
396    pub class_name: String,
397    pub dtype: Option<String>,
398    pub shape: Vec<usize>,
399    pub is_gpu: bool,
400    pub residency: WorkspaceResidency,
401    pub size_bytes: Option<u64>,
402    pub preview: Option<WorkspacePreview>,
403    pub value: Value,
404}
405
406#[derive(Debug, Clone)]
407pub enum WorkspaceMaterializeTarget {
408    Name(String),
409    Token(Uuid),
410}
411
412#[derive(Debug, Clone)]
413pub struct WorkspaceSliceOptions {
414    pub start: Vec<usize>,
415    pub shape: Vec<usize>,
416}
417
418impl WorkspaceSliceOptions {
419    fn sanitized(&self, tensor_shape: &[usize]) -> Option<WorkspaceSliceOptions> {
420        if tensor_shape.is_empty() {
421            return None;
422        }
423        let mut start = Vec::with_capacity(tensor_shape.len());
424        let mut shape = Vec::with_capacity(tensor_shape.len());
425        for (axis_idx, axis_len) in tensor_shape.iter().enumerate() {
426            let axis_len = *axis_len;
427            if axis_len == 0 {
428                return None;
429            }
430            let requested_start = self.start.get(axis_idx).copied().unwrap_or(0);
431            let clamped_start = requested_start.min(axis_len.saturating_sub(1));
432            let requested_count = self.shape.get(axis_idx).copied().unwrap_or(axis_len);
433            let clamped_count = requested_count.max(1).min(axis_len - clamped_start);
434            start.push(clamped_start);
435            shape.push(clamped_count);
436        }
437        Some(WorkspaceSliceOptions { start, shape })
438    }
439}
440
441#[derive(Debug, Clone)]
442pub struct WorkspaceMaterializeOptions {
443    pub max_elements: usize,
444    pub slice: Option<WorkspaceSliceOptions>,
445}
446
447impl Default for WorkspaceMaterializeOptions {
448    fn default() -> Self {
449        Self {
450            max_elements: MATERIALIZE_DEFAULT_LIMIT,
451            slice: None,
452        }
453    }
454}
455
456fn slice_value_for_preview(value: &Value, slice: &WorkspaceSliceOptions) -> Option<Value> {
457    match value {
458        Value::Tensor(tensor) => {
459            let data = gather_tensor_slice(tensor, slice);
460            if data.is_empty() {
461                return None;
462            }
463            let mut shape = slice.shape.clone();
464            if shape.is_empty() {
465                shape.push(1);
466            }
467            let rows = shape.first().copied().unwrap_or(1);
468            let cols = shape.get(1).copied().unwrap_or(1);
469            Some(Value::Tensor(Tensor {
470                data,
471                shape,
472                rows,
473                cols,
474                dtype: tensor.dtype,
475            }))
476        }
477        _ => None,
478    }
479}
480
481fn gather_tensor_slice(tensor: &Tensor, slice: &WorkspaceSliceOptions) -> Vec<f64> {
482    if tensor.shape.is_empty() || slice.shape.contains(&0) {
483        return Vec::new();
484    }
485    let total: usize = slice.shape.iter().product();
486    let mut result = Vec::with_capacity(total);
487    let mut coords = vec![0usize; tensor.shape.len()];
488    gather_tensor_slice_recursive(tensor, slice, 0, &mut coords, &mut result);
489    result
490}
491
492fn gather_tensor_slice_recursive(
493    tensor: &Tensor,
494    slice: &WorkspaceSliceOptions,
495    axis: usize,
496    coords: &mut [usize],
497    out: &mut Vec<f64>,
498) {
499    if axis == tensor.shape.len() {
500        let idx = column_major_index(&tensor.shape, coords);
501        if let Some(value) = tensor.data.get(idx) {
502            out.push(*value);
503        }
504        return;
505    }
506    let start = slice.start.get(axis).copied().unwrap_or(0);
507    let count = slice.shape.get(axis).copied().unwrap_or(1);
508    for offset in 0..count {
509        coords[axis] = start + offset;
510        gather_tensor_slice_recursive(tensor, slice, axis + 1, coords, out);
511    }
512}
513
514fn column_major_index(shape: &[usize], coords: &[usize]) -> usize {
515    let mut idx = 0usize;
516    let mut stride = 1usize;
517    for (dim_len, coord) in shape.iter().zip(coords.iter()) {
518        idx += coord * stride;
519        stride *= *dim_len;
520    }
521    idx
522}
523
524fn visit_slice_coords<F: FnMut(&[usize])>(
525    full_shape: &[usize],
526    slice: &WorkspaceSliceOptions,
527    axis: usize,
528    coords: &mut [usize],
529    f: &mut F,
530) {
531    if axis == full_shape.len() {
532        f(coords);
533        return;
534    }
535    let start = slice.start.get(axis).copied().unwrap_or(0);
536    let count = slice.shape.get(axis).copied().unwrap_or(1);
537    for offset in 0..count {
538        coords[axis] = start + offset;
539        visit_slice_coords(full_shape, slice, axis + 1, coords, f);
540    }
541}
542
543fn gpu_dtype_label(handle: &runmat_accelerate_api::GpuTensorHandle) -> Option<&'static str> {
544    let precision = runmat_accelerate_api::handle_precision(handle)
545        .unwrap_or(runmat_accelerate_api::ProviderPrecision::F64);
546    match precision {
547        ProviderPrecision::F32 => Some("single"),
548        ProviderPrecision::F64 => Some("double"),
549    }
550}
551
552fn gpu_size_bytes(handle: &runmat_accelerate_api::GpuTensorHandle) -> Option<u64> {
553    let precision = runmat_accelerate_api::handle_precision(handle)
554        .unwrap_or(runmat_accelerate_api::ProviderPrecision::F64);
555    let element_size = match precision {
556        ProviderPrecision::F32 => 4u64,
557        ProviderPrecision::F64 => 8u64,
558    };
559    let elements: u64 = handle
560        .shape
561        .iter()
562        .try_fold(1u64, |acc, &d| acc.checked_mul(d as u64))?;
563    elements.checked_mul(element_size)
564}
565
566async fn gather_gpu_preview_values(
567    handle: &runmat_accelerate_api::GpuTensorHandle,
568    full_shape: &[usize],
569    options: &WorkspaceMaterializeOptions,
570) -> Result<Option<(Vec<f64>, bool)>> {
571    if full_shape.is_empty() || full_shape.contains(&0) {
572        return Ok(None);
573    }
574    let total_elements = full_shape.iter().product::<usize>();
575    if total_elements == 0 {
576        return Ok(None);
577    }
578
579    let provider = provider_for_handle(handle)
580        .ok_or_else(|| anyhow::anyhow!("No acceleration provider registered for GPU tensor"))?;
581
582    // Determine which indices to gather.
583    let (indices, output_shape, truncated) = if let Some(slice) = options
584        .slice
585        .as_ref()
586        .and_then(|slice| slice.sanitized(full_shape))
587    {
588        let slice_elements = slice.shape.iter().product::<usize>();
589        let requested = slice_elements.min(options.max_elements.max(1));
590        let mut indices: Vec<u32> = Vec::with_capacity(requested);
591        let mut coords = vec![0usize; full_shape.len()];
592        let mut produced = 0usize;
593        let mut push_idx = |coords: &[usize]| {
594            if produced >= requested {
595                return;
596            }
597            let idx = column_major_index(full_shape, coords);
598            if idx <= u32::MAX as usize {
599                indices.push(idx as u32);
600                produced += 1;
601            }
602        };
603        visit_slice_coords(full_shape, &slice, 0, &mut coords, &mut push_idx);
604        let truncated = requested < slice_elements;
605        let output_shape = if !truncated && indices.len() == slice_elements {
606            slice.shape
607        } else {
608            vec![indices.len().max(1), 1]
609        };
610        (indices, output_shape, truncated)
611    } else {
612        let count = total_elements.min(options.max_elements.max(1));
613        let mut indices: Vec<u32> = Vec::with_capacity(count);
614        for idx in 0..count {
615            if idx > u32::MAX as usize {
616                break;
617            }
618            indices.push(idx as u32);
619        }
620        let len = indices.len();
621        let truncated = total_elements > len;
622        (indices, vec![len.max(1), 1], truncated)
623    };
624
625    if indices.is_empty() {
626        return Ok(None);
627    }
628
629    // Gather a small GPU tensor, then download it.
630    let gathered = provider
631        .gather_linear(handle, &indices, &output_shape)
632        .map_err(|e| anyhow::anyhow!("gpu preview gather_linear: {e}"))?;
633    let host = provider
634        .download(&gathered)
635        .await
636        .map_err(|e| anyhow::anyhow!("gpu preview download: {e}"))?;
637    // Best-effort cleanup.
638    let _ = provider.free(&gathered);
639
640    Ok(Some((host.data, truncated)))
641}
642
643#[derive(Debug, Clone, Default)]
644pub struct ExecutionProfiling {
645    pub total_ms: u64,
646    pub cpu_ms: Option<u64>,
647    pub gpu_ms: Option<u64>,
648    pub kernel_count: Option<u32>,
649}
650
651#[derive(Debug, Clone, Default)]
652pub struct FusionPlanSnapshot {
653    pub nodes: Vec<FusionPlanNode>,
654    pub edges: Vec<FusionPlanEdge>,
655    pub shaders: Vec<FusionPlanShader>,
656    pub decisions: Vec<FusionPlanDecision>,
657}
658
659#[derive(Debug, Clone)]
660pub struct FusionPlanNode {
661    pub id: String,
662    pub kind: String,
663    pub label: String,
664    pub shape: Vec<usize>,
665    pub residency: Option<String>,
666}
667
668#[derive(Debug, Clone)]
669pub struct FusionPlanEdge {
670    pub from: String,
671    pub to: String,
672    pub reason: Option<String>,
673}
674
675#[derive(Debug, Clone)]
676pub struct FusionPlanShader {
677    pub name: String,
678    pub stage: String,
679    pub workgroup_size: Option<[u32; 3]>,
680    pub source_hash: Option<String>,
681}
682
683#[derive(Debug, Clone)]
684pub struct FusionPlanDecision {
685    pub node_id: String,
686    pub fused: bool,
687    pub reason: Option<String>,
688    pub thresholds: Option<String>,
689}
690
691#[derive(Debug)]
692pub struct ExecutionResult {
693    pub value: Option<Value>,
694    pub execution_time_ms: u64,
695    pub used_jit: bool,
696    pub error: Option<RuntimeError>,
697    /// Type information displayed when output is suppressed by semicolon
698    pub type_info: Option<String>,
699    /// Ordered console output (stdout/stderr) captured during execution.
700    pub streams: Vec<ExecutionStreamEntry>,
701    /// Workspace metadata for variables touched during this execution.
702    pub workspace: WorkspaceSnapshot,
703    /// Figure handles that were mutated during this execution.
704    pub figures_touched: Vec<u32>,
705    /// Structured MATLAB warnings raised during this execution.
706    pub warnings: Vec<RuntimeWarning>,
707    /// Optional profiling summary (wall/cpu/gpu).
708    pub profiling: Option<ExecutionProfiling>,
709    /// Optional fusion plan metadata emitted by Accelerate.
710    pub fusion_plan: Option<FusionPlanSnapshot>,
711    /// Recorded stdin interactions (prompts, values) during execution.
712    pub stdin_events: Vec<StdinEvent>,
713}
714
715#[derive(Debug, Clone)]
716struct WorkspaceMaterializeTicket {
717    name: String,
718}
719
720#[derive(Debug, Clone, Copy, PartialEq, Eq)]
721enum FinalStmtEmitDisposition {
722    Inline,
723    #[allow(dead_code)]
724    NeedsFallback,
725    Suppressed,
726}
727
728fn determine_display_label_from_context(
729    single_assign_var: Option<usize>,
730    id_to_name: &HashMap<usize, String>,
731    is_expression_stmt: bool,
732    single_stmt_non_assign: bool,
733) -> Option<String> {
734    if let Some(var_id) = single_assign_var {
735        id_to_name.get(&var_id).cloned()
736    } else if is_expression_stmt || single_stmt_non_assign {
737        Some("ans".to_string())
738    } else {
739        None
740    }
741}
742
743/// Format value type information like MATLAB (e.g., "1000x1 vector", "3x3 matrix")
744fn format_type_info(value: &Value) -> String {
745    match value {
746        Value::Int(_) => "scalar".to_string(),
747        Value::Num(_) => "scalar".to_string(),
748        Value::Bool(_) => "logical scalar".to_string(),
749        Value::String(_) => "string".to_string(),
750        Value::StringArray(sa) => {
751            // MATLAB displays string arrays as m x n string array; for test's purpose, we classify scalar string arrays as "string"
752            if sa.shape == vec![1, 1] {
753                "string".to_string()
754            } else if sa.shape.len() > 2 {
755                let dims: Vec<String> = sa.shape.iter().map(|d| d.to_string()).collect();
756                format!("{} string array", dims.join("x"))
757            } else {
758                format!("{}x{} string array", sa.rows(), sa.cols())
759            }
760        }
761        Value::CharArray(ca) => {
762            if ca.rows == 1 && ca.cols == 1 {
763                "char".to_string()
764            } else {
765                format!("{}x{} char array", ca.rows, ca.cols)
766            }
767        }
768        Value::Tensor(m) => {
769            if m.rows() == 1 && m.cols() == 1 {
770                "scalar".to_string()
771            } else if m.rows() == 1 || m.cols() == 1 {
772                format!("{}x{} vector", m.rows(), m.cols())
773            } else {
774                format!("{}x{} matrix", m.rows(), m.cols())
775            }
776        }
777        Value::Cell(cells) => {
778            if cells.data.len() == 1 {
779                "1x1 cell".to_string()
780            } else {
781                format!("{}x1 cell array", cells.data.len())
782            }
783        }
784        Value::GpuTensor(h) => {
785            if h.shape.len() == 2 {
786                let r = h.shape[0];
787                let c = h.shape[1];
788                if r == 1 && c == 1 {
789                    "scalar (gpu)".to_string()
790                } else if r == 1 || c == 1 {
791                    format!("{r}x{c} vector (gpu)")
792                } else {
793                    format!("{r}x{c} matrix (gpu)")
794                }
795            } else {
796                format!("Tensor{:?} (gpu)", h.shape)
797            }
798        }
799        _ => "value".to_string(),
800    }
801}
802
803impl RunMatSession {
804    /// Create a new session
805    pub fn new() -> Result<Self> {
806        Self::with_options(true, false) // JIT enabled, verbose disabled
807    }
808
809    /// Create a new session with specific options
810    pub fn with_options(enable_jit: bool, verbose: bool) -> Result<Self> {
811        Self::from_snapshot(enable_jit, verbose, None)
812    }
813
814    /// Create a new session with snapshot loading
815    #[cfg(not(target_arch = "wasm32"))]
816    pub fn with_snapshot<P: AsRef<Path>>(
817        enable_jit: bool,
818        verbose: bool,
819        snapshot_path: Option<P>,
820    ) -> Result<Self> {
821        let snapshot = snapshot_path.and_then(|path| match Self::load_snapshot(path.as_ref()) {
822            Ok(snapshot) => {
823                info!(
824                    "Snapshot loaded successfully from {}",
825                    path.as_ref().display()
826                );
827                Some(Arc::new(snapshot))
828            }
829            Err(e) => {
830                warn!(
831                    "Failed to load snapshot from {}: {}, continuing without snapshot",
832                    path.as_ref().display(),
833                    e
834                );
835                None
836            }
837        });
838        Self::from_snapshot(enable_jit, verbose, snapshot)
839    }
840
841    /// Create a session using snapshot bytes (already fetched from disk or network)
842    pub fn with_snapshot_bytes(
843        enable_jit: bool,
844        verbose: bool,
845        snapshot_bytes: Option<&[u8]>,
846    ) -> Result<Self> {
847        let snapshot =
848            snapshot_bytes.and_then(|bytes| match Self::load_snapshot_from_bytes(bytes) {
849                Ok(snapshot) => {
850                    info!("Snapshot loaded successfully from in-memory bytes");
851                    Some(Arc::new(snapshot))
852                }
853                Err(e) => {
854                    warn!("Failed to load snapshot from bytes: {e}, continuing without snapshot");
855                    None
856                }
857            });
858        Self::from_snapshot(enable_jit, verbose, snapshot)
859    }
860
861    fn from_snapshot(
862        enable_jit: bool,
863        verbose: bool,
864        snapshot: Option<Arc<Snapshot>>,
865    ) -> Result<Self> {
866        #[cfg(target_arch = "wasm32")]
867        let snapshot = {
868            match snapshot {
869                some @ Some(_) => some,
870                None => Self::build_wasm_snapshot(),
871            }
872        };
873
874        #[cfg(feature = "jit")]
875        let jit_engine = if enable_jit {
876            match TurbineEngine::new() {
877                Ok(engine) => {
878                    info!("JIT compiler initialized successfully");
879                    Some(engine)
880                }
881                Err(e) => {
882                    warn!("JIT compiler initialization failed: {e}, falling back to interpreter");
883                    None
884                }
885            }
886        } else {
887            info!("JIT compiler disabled, using interpreter only");
888            None
889        };
890
891        #[cfg(not(feature = "jit"))]
892        if enable_jit {
893            info!("JIT support was requested but the 'jit' feature is disabled; running interpreter-only.");
894        }
895
896        let session = Self {
897            #[cfg(feature = "jit")]
898            jit_engine,
899            verbose,
900            stats: ExecutionStats::default(),
901            variables: HashMap::new(),
902            variable_array: Vec::new(),
903            variable_names: HashMap::new(),
904            workspace_values: HashMap::new(),
905            function_definitions: HashMap::new(),
906            source_pool: SourcePool::default(),
907            function_source_ids: HashMap::new(),
908            snapshot,
909            interrupt_flag: Arc::new(AtomicBool::new(false)),
910            is_executing: false,
911            async_input_handler: None,
912            callstack_limit: runmat_ignition::DEFAULT_CALLSTACK_LIMIT,
913            error_namespace: runmat_ignition::DEFAULT_ERROR_NAMESPACE.to_string(),
914            default_source_name: "<repl>".to_string(),
915            source_name_override: None,
916            telemetry_consent: true,
917            telemetry_client_id: None,
918            telemetry_platform: TelemetryPlatformInfo::default(),
919            telemetry_sink: None,
920            workspace_preview_tokens: HashMap::new(),
921            workspace_version: 0,
922            emit_fusion_plan: false,
923            compat_mode: CompatMode::Matlab,
924        };
925
926        runmat_ignition::set_call_stack_limit(session.callstack_limit);
927
928        // Cache the shared plotting context (if a GPU provider is active) so the
929        // runtime can wire zero-copy render paths without instantiating another
930        // WebGPU device.
931        #[cfg(any(target_arch = "wasm32", not(target_arch = "wasm32")))]
932        {
933            if let Err(err) =
934                runmat_runtime::builtins::plotting::context::ensure_context_from_provider()
935            {
936                debug!("Plotting context unavailable during session init: {err}");
937            }
938        }
939
940        Ok(session)
941    }
942
943    fn current_source_name(&self) -> &str {
944        self.source_name_override
945            .as_deref()
946            .unwrap_or(&self.default_source_name)
947    }
948
949    #[cfg(target_arch = "wasm32")]
950    fn build_wasm_snapshot() -> Option<Arc<Snapshot>> {
951        use log::{info, warn};
952
953        info!("No snapshot provided; building stdlib snapshot inside wasm runtime");
954        let config = SnapshotConfig {
955            compression_enabled: false,
956            validation_enabled: false,
957            memory_mapping_enabled: false,
958            parallel_loading: false,
959            progress_reporting: false,
960            ..Default::default()
961        };
962
963        match SnapshotBuilder::new(config).build() {
964            Ok(snapshot) => {
965                info!("WASM snapshot build completed successfully");
966                Some(Arc::new(snapshot))
967            }
968            Err(err) => {
969                warn!("Failed to build stdlib snapshot in wasm runtime: {err}");
970                None
971            }
972        }
973    }
974
975    /// Load a snapshot from disk
976    #[cfg(not(target_arch = "wasm32"))]
977    fn load_snapshot(path: &Path) -> Result<Snapshot> {
978        let mut loader = SnapshotLoader::new(SnapshotConfig::default());
979        let (snapshot, _stats) = loader
980            .load(path)
981            .map_err(|e| anyhow::anyhow!("Failed to load snapshot: {}", e))?;
982        Ok(snapshot)
983    }
984
985    /// Load a snapshot from in-memory bytes
986    fn load_snapshot_from_bytes(bytes: &[u8]) -> Result<Snapshot> {
987        let mut loader = SnapshotLoader::new(SnapshotConfig::default());
988        let (snapshot, _stats) = loader
989            .load_from_bytes(bytes)
990            .map_err(|e| anyhow::anyhow!("Failed to load snapshot: {}", e))?;
991        Ok(snapshot)
992    }
993
994    /// Install an async stdin handler (Phase 2). This is the preferred input path for
995    /// poll-driven execution (`ExecuteFuture`).
996    ///
997    /// The handler is invoked when `input()` / `pause()` needs a line or keypress, and the
998    /// returned future is awaited by the runtime.
999    pub fn install_async_input_handler<F, Fut>(&mut self, handler: F)
1000    where
1001        F: Fn(InputRequest) -> Fut + Send + Sync + 'static,
1002        Fut: Future<Output = Result<InputResponse, String>> + 'static,
1003    {
1004        self.async_input_handler = Some(Arc::new(move |req: InputRequest| {
1005            let fut = handler(req);
1006            Box::pin(fut)
1007        }));
1008    }
1009
1010    pub fn clear_async_input_handler(&mut self) {
1011        self.async_input_handler = None;
1012    }
1013
1014    pub fn telemetry_consent(&self) -> bool {
1015        self.telemetry_consent
1016    }
1017
1018    pub fn set_telemetry_consent(&mut self, consent: bool) {
1019        self.telemetry_consent = consent;
1020    }
1021
1022    pub fn telemetry_client_id(&self) -> Option<&str> {
1023        self.telemetry_client_id.as_deref()
1024    }
1025
1026    pub fn set_telemetry_client_id(&mut self, cid: Option<String>) {
1027        self.telemetry_client_id = cid;
1028    }
1029
1030    /// Request cooperative cancellation for the currently running execution.
1031    pub fn cancel_execution(&self) {
1032        self.interrupt_flag.store(true, Ordering::Relaxed);
1033    }
1034
1035    /// Shared interrupt flag used by the VM to implement cooperative cancellation.
1036    pub fn interrupt_handle(&self) -> Arc<AtomicBool> {
1037        Arc::clone(&self.interrupt_flag)
1038    }
1039
1040    /// Get snapshot information
1041    pub fn snapshot_info(&self) -> Option<String> {
1042        self.snapshot.as_ref().map(|snapshot| {
1043            format!(
1044                "Snapshot loaded: {} builtins, {} HIR functions, {} bytecode entries",
1045                snapshot.builtins.functions.len(),
1046                snapshot.hir_cache.functions.len(),
1047                snapshot.bytecode_cache.stdlib_bytecode.len()
1048            )
1049        })
1050    }
1051
1052    /// Check if a snapshot is loaded
1053    pub fn has_snapshot(&self) -> bool {
1054        self.snapshot.is_some()
1055    }
1056
1057    fn compile_input(&mut self, input: &str) -> std::result::Result<PreparedExecution, RunError> {
1058        let source_name = self.current_source_name().to_string();
1059        let source_id = self.source_pool.intern(&source_name, input);
1060        let ast = {
1061            let _span = info_span!("runtime.parse").entered();
1062            parse_with_options(input, ParserOptions::new(self.compat_mode))?
1063        };
1064        let lowering = {
1065            let _span = info_span!("runtime.lower").entered();
1066            runmat_hir::lower(
1067                &ast,
1068                &LoweringContext::new(&self.variable_names, &self.function_definitions),
1069            )?
1070        };
1071        let existing_functions = self.convert_hir_functions_to_user_functions();
1072        let mut bytecode = {
1073            let _span = info_span!("runtime.compile.bytecode").entered();
1074            runmat_ignition::compile(&lowering.hir, &existing_functions)?
1075        };
1076        bytecode.source_id = Some(source_id);
1077        let new_function_names: HashSet<String> = lowering.functions.keys().cloned().collect();
1078        for (name, func) in bytecode.functions.iter_mut() {
1079            if new_function_names.contains(name) {
1080                func.source_id = Some(source_id);
1081            }
1082        }
1083        Ok(PreparedExecution {
1084            ast,
1085            lowering,
1086            bytecode,
1087        })
1088    }
1089
1090    fn populate_callstack(&self, error: &mut RuntimeError) {
1091        if !error.context.call_stack.is_empty() || error.context.call_frames.is_empty() {
1092            return;
1093        }
1094        let mut rendered = Vec::new();
1095        if error.context.call_frames_elided > 0 {
1096            rendered.push(format!(
1097                "... {} frames elided ...",
1098                error.context.call_frames_elided
1099            ));
1100        }
1101        for frame in error.context.call_frames.iter().rev() {
1102            let mut line = frame.function.clone();
1103            if let (Some(source_id), Some((start, _end))) = (frame.source_id, frame.span) {
1104                if let Some(source) = self.source_pool.get(SourceId(source_id)) {
1105                    let (line_num, col) = line_col_from_offset(&source.text, start);
1106                    line = format!("{} @ {}:{}:{}", frame.function, source.name, line_num, col);
1107                }
1108            }
1109            rendered.push(line);
1110        }
1111        error.context.call_stack = rendered;
1112    }
1113
1114    fn normalize_error_namespace(&self, error: &mut RuntimeError) {
1115        let Some(identifier) = error.identifier.clone() else {
1116            return;
1117        };
1118        let suffix = identifier
1119            .split_once(':')
1120            .map(|(_, suffix)| suffix)
1121            .unwrap_or(identifier.as_str());
1122        error.identifier = Some(format!("{}:{suffix}", self.error_namespace));
1123    }
1124
1125    /// Compile the input and produce a fusion plan snapshot without executing.
1126    pub fn compile_fusion_plan(
1127        &mut self,
1128        input: &str,
1129    ) -> std::result::Result<Option<FusionPlanSnapshot>, RunError> {
1130        let prepared = self.compile_input(input)?;
1131        Ok(build_fusion_snapshot(
1132            prepared.bytecode.accel_graph.as_ref(),
1133            &prepared.bytecode.fusion_groups,
1134        ))
1135    }
1136
1137    /// Execute MATLAB/Octave code
1138    pub async fn execute(&mut self, input: &str) -> std::result::Result<ExecutionResult, RunError> {
1139        self.run(input).await
1140    }
1141
1142    /// Parse, lower, compile, and execute input.
1143    pub async fn run(&mut self, input: &str) -> std::result::Result<ExecutionResult, RunError> {
1144        let _active = ActiveExecutionGuard::new(self).map_err(|err| {
1145            RunError::Runtime(
1146                build_runtime_error(err.to_string())
1147                    .with_identifier("RunMat:ExecutionAlreadyActive")
1148                    .build(),
1149            )
1150        })?;
1151        runmat_ignition::set_call_stack_limit(self.callstack_limit);
1152        runmat_ignition::set_error_namespace(&self.error_namespace);
1153        runmat_hir::set_error_namespace(&self.error_namespace);
1154        let exec_span = info_span!(
1155            "runtime.execute",
1156            input_len = input.len(),
1157            verbose = self.verbose
1158        );
1159        let _exec_guard = exec_span.enter();
1160        runmat_runtime::console::reset_thread_buffer();
1161        runmat_runtime::plotting_hooks::reset_recent_figures();
1162        runmat_runtime::warning_store::reset();
1163        reset_provider_telemetry();
1164        self.interrupt_flag.store(false, Ordering::Relaxed);
1165        let _interrupt_guard =
1166            runmat_runtime::interrupt::replace_interrupt(Some(self.interrupt_flag.clone()));
1167        let start_time = Instant::now();
1168        self.stats.total_executions += 1;
1169        let debug_trace = std::env::var("RUNMAT_DEBUG_REPL").is_ok();
1170        let stdin_events: Arc<Mutex<Vec<StdinEvent>>> = Arc::new(Mutex::new(Vec::new()));
1171        let host_async_handler = self.async_input_handler.clone();
1172        let stdin_events_async = Arc::clone(&stdin_events);
1173        let runtime_async_handler: Arc<runmat_runtime::interaction::AsyncInteractionHandler> =
1174            Arc::new(
1175                move |prompt: runmat_runtime::interaction::InteractionPromptOwned| {
1176                    let request_kind = match prompt.kind {
1177                        runmat_runtime::interaction::InteractionKind::Line { echo } => {
1178                            InputRequestKind::Line { echo }
1179                        }
1180                        runmat_runtime::interaction::InteractionKind::KeyPress => {
1181                            InputRequestKind::KeyPress
1182                        }
1183                    };
1184                    let request = InputRequest {
1185                        prompt: prompt.prompt,
1186                        kind: request_kind,
1187                    };
1188                    let (event_kind, echo_flag) = match &request.kind {
1189                        InputRequestKind::Line { echo } => (StdinEventKind::Line, *echo),
1190                        InputRequestKind::KeyPress => (StdinEventKind::KeyPress, false),
1191                    };
1192                    let mut event = StdinEvent {
1193                        prompt: request.prompt.clone(),
1194                        kind: event_kind,
1195                        echo: echo_flag,
1196                        value: None,
1197                        error: None,
1198                    };
1199
1200                    let stdin_events_async = Arc::clone(&stdin_events_async);
1201                    let host_async_handler = host_async_handler.clone();
1202                    Box::pin(async move {
1203                        let resp: Result<InputResponse, String> =
1204                            if let Some(handler) = host_async_handler {
1205                                handler(request).await
1206                            } else {
1207                                match &request.kind {
1208                                    InputRequestKind::Line { echo } => {
1209                                        runmat_runtime::interaction::default_read_line(
1210                                            &request.prompt,
1211                                            *echo,
1212                                        )
1213                                        .map(InputResponse::Line)
1214                                    }
1215                                    InputRequestKind::KeyPress => {
1216                                        runmat_runtime::interaction::default_wait_for_key(
1217                                            &request.prompt,
1218                                        )
1219                                        .map(|_| InputResponse::KeyPress)
1220                                    }
1221                                }
1222                            };
1223
1224                        let resp = resp.inspect_err(|err| {
1225                            event.error = Some(err.clone());
1226                            if let Ok(mut guard) = stdin_events_async.lock() {
1227                                guard.push(event.clone());
1228                            }
1229                        })?;
1230
1231                        let interaction_resp = match resp {
1232                            InputResponse::Line(value) => {
1233                                event.value = Some(value.clone());
1234                                if let Ok(mut guard) = stdin_events_async.lock() {
1235                                    guard.push(event);
1236                                }
1237                                runmat_runtime::interaction::InteractionResponse::Line(value)
1238                            }
1239                            InputResponse::KeyPress => {
1240                                if let Ok(mut guard) = stdin_events_async.lock() {
1241                                    guard.push(event);
1242                                }
1243                                runmat_runtime::interaction::InteractionResponse::KeyPress
1244                            }
1245                        };
1246                        Ok(interaction_resp)
1247                    })
1248                },
1249            );
1250        let _async_input_guard =
1251            runmat_runtime::interaction::replace_async_handler(Some(runtime_async_handler));
1252
1253        if self.verbose {
1254            debug!("Executing: {}", input.trim());
1255        }
1256
1257        let _source_guard = runmat_runtime::source_context::replace_current_source(Some(input));
1258
1259        let PreparedExecution {
1260            ast,
1261            lowering,
1262            mut bytecode,
1263        } = self.compile_input(input)?;
1264        if self.verbose {
1265            debug!("AST: {ast:?}");
1266        }
1267        let (hir, updated_vars, updated_functions, var_names_map) = (
1268            lowering.hir,
1269            lowering.variables,
1270            lowering.functions,
1271            lowering.var_names,
1272        );
1273        let max_var_id = updated_vars.values().copied().max().unwrap_or(0);
1274        if debug_trace {
1275            debug!(?updated_vars, "[repl] updated_vars");
1276        }
1277        if debug_trace {
1278            debug!(workspace_values_before = ?self.workspace_values, "[repl] workspace snapshot before execution");
1279        }
1280        let id_to_name: HashMap<usize, String> = var_names_map
1281            .iter()
1282            .map(|(var_id, name)| (var_id.0, name.clone()))
1283            .collect();
1284        let mut assigned_this_execution: HashSet<String> = HashSet::new();
1285        let assigned_snapshot: HashSet<String> = updated_vars
1286            .keys()
1287            .filter(|name| self.workspace_values.contains_key(name.as_str()))
1288            .cloned()
1289            .collect();
1290        let prev_assigned_snapshot = assigned_snapshot.clone();
1291        if debug_trace {
1292            debug!(?assigned_snapshot, "[repl] assigned snapshot");
1293        }
1294        let _pending_workspace_guard = runmat_ignition::push_pending_workspace(
1295            updated_vars.clone(),
1296            assigned_snapshot.clone(),
1297        );
1298        if self.verbose {
1299            debug!("HIR generated successfully");
1300        }
1301
1302        let (single_assign_var, single_stmt_non_assign) = if hir.body.len() == 1 {
1303            match &hir.body[0] {
1304                runmat_hir::HirStmt::Assign(var_id, _, _, _) => (Some(var_id.0), false),
1305                _ => (None, true),
1306            }
1307        } else {
1308            (None, false)
1309        };
1310
1311        bytecode.var_names = id_to_name.clone();
1312        if self.verbose {
1313            debug!(
1314                "Bytecode compiled: {} instructions",
1315                bytecode.instructions.len()
1316            );
1317        }
1318
1319        #[cfg(not(target_arch = "wasm32"))]
1320        let fusion_snapshot = if self.emit_fusion_plan {
1321            build_fusion_snapshot(bytecode.accel_graph.as_ref(), &bytecode.fusion_groups)
1322        } else {
1323            None
1324        };
1325        #[cfg(target_arch = "wasm32")]
1326        let fusion_snapshot: Option<FusionPlanSnapshot> = None;
1327
1328        // Prepare variable array with existing values before execution
1329        self.prepare_variable_array_for_execution(&bytecode, &updated_vars, debug_trace);
1330
1331        if self.verbose {
1332            debug!(
1333                "Variable array after preparation: {:?}",
1334                self.variable_array
1335            );
1336            debug!("Updated variable mapping: {updated_vars:?}");
1337            debug!("Bytecode instructions: {:?}", bytecode.instructions);
1338        }
1339
1340        #[cfg(feature = "jit")]
1341        let mut used_jit = false;
1342        #[cfg(not(feature = "jit"))]
1343        let used_jit = false;
1344        #[cfg(feature = "jit")]
1345        let mut execution_completed = false;
1346        #[cfg(not(feature = "jit"))]
1347        let execution_completed = false;
1348        let mut result_value: Option<Value> = None; // Always start fresh for each execution
1349        let mut suppressed_value: Option<Value> = None; // Track value for type info when suppressed
1350        let mut error = None;
1351        let mut workspace_updates: Vec<WorkspaceEntry> = Vec::new();
1352        let mut workspace_snapshot_force_full = false;
1353        let mut ans_update: Option<(usize, Value)> = None;
1354
1355        // Check if this is an expression statement (ends with Pop)
1356        let is_expression_stmt = bytecode
1357            .instructions
1358            .last()
1359            .map(|instr| matches!(instr, runmat_ignition::Instr::Pop))
1360            .unwrap_or(false);
1361
1362        // Determine whether the final statement ended with a semicolon by inspecting the raw input.
1363        let is_semicolon_suppressed = {
1364            let toks = tokenize_detailed(input);
1365            toks.into_iter()
1366                .rev()
1367                .map(|t| t.token)
1368                .find(|token| {
1369                    !matches!(
1370                        token,
1371                        LexToken::Newline
1372                            | LexToken::LineComment
1373                            | LexToken::BlockComment
1374                            | LexToken::Section
1375                    )
1376                })
1377                .map(|t| matches!(t, LexToken::Semicolon))
1378                .unwrap_or(false)
1379        };
1380        let final_stmt_emit = last_displayable_statement_emit_disposition(&hir.body);
1381
1382        if self.verbose {
1383            debug!("HIR body len: {}", hir.body.len());
1384            if !hir.body.is_empty() {
1385                debug!("HIR statement: {:?}", &hir.body[0]);
1386            }
1387            debug!("is_semicolon_suppressed: {is_semicolon_suppressed}");
1388        }
1389
1390        // Use JIT for assignments, interpreter for expressions (to capture results properly)
1391        #[cfg(feature = "jit")]
1392        {
1393            if let Some(ref mut jit_engine) = &mut self.jit_engine {
1394                if !is_expression_stmt {
1395                    // Ensure variable array is large enough
1396                    if self.variable_array.len() < bytecode.var_count {
1397                        self.variable_array
1398                            .resize(bytecode.var_count, Value::Num(0.0));
1399                    }
1400
1401                    if self.verbose {
1402                        debug!(
1403                            "JIT path for assignment: variable_array size: {}, bytecode.var_count: {}",
1404                            self.variable_array.len(),
1405                            bytecode.var_count
1406                        );
1407                    }
1408
1409                    // Use JIT for assignments
1410                    match jit_engine.execute_or_compile(&bytecode, &mut self.variable_array) {
1411                        Ok((_, actual_used_jit)) => {
1412                            used_jit = actual_used_jit;
1413                            execution_completed = true;
1414                            if actual_used_jit {
1415                                self.stats.jit_compiled += 1;
1416                            } else {
1417                                self.stats.interpreter_fallback += 1;
1418                            }
1419                            if let Some(runmat_hir::HirStmt::Assign(var_id, _, _, _)) =
1420                                hir.body.first()
1421                            {
1422                                if let Some(name) = id_to_name.get(&var_id.0) {
1423                                    assigned_this_execution.insert(name.clone());
1424                                }
1425                                if var_id.0 < self.variable_array.len() {
1426                                    let assignment_value = self.variable_array[var_id.0].clone();
1427                                    if !is_semicolon_suppressed {
1428                                        result_value = Some(assignment_value);
1429                                        if self.verbose {
1430                                            debug!("JIT assignment result: {result_value:?}");
1431                                        }
1432                                    } else {
1433                                        suppressed_value = Some(assignment_value);
1434                                        if self.verbose {
1435                                            debug!("JIT assignment suppressed due to semicolon, captured for type info");
1436                                        }
1437                                    }
1438                                }
1439                            }
1440
1441                            if self.verbose {
1442                                debug!(
1443                                    "{} assignment successful, variable_array: {:?}",
1444                                    if actual_used_jit {
1445                                        "JIT"
1446                                    } else {
1447                                        "Interpreter"
1448                                    },
1449                                    self.variable_array
1450                                );
1451                            }
1452                        }
1453                        Err(e) => {
1454                            if self.verbose {
1455                                debug!("JIT execution failed: {e}, using interpreter");
1456                            }
1457                            // Fall back to interpreter
1458                        }
1459                    }
1460                }
1461            }
1462        }
1463
1464        // Use interpreter if JIT failed or is disabled
1465        if !execution_completed {
1466            if self.verbose {
1467                debug!(
1468                    "Interpreter path: variable_array size: {}, bytecode.var_count: {}",
1469                    self.variable_array.len(),
1470                    bytecode.var_count
1471                );
1472            }
1473
1474            // For expressions, modify bytecode to store result in a temp variable instead of using stack
1475            let mut execution_bytecode = bytecode.clone();
1476            if is_expression_stmt
1477                && matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)
1478                && !execution_bytecode.instructions.is_empty()
1479            {
1480                execution_bytecode.instructions.pop(); // Remove the Pop instruction
1481
1482                // Add StoreVar instruction to store the result in a temporary variable
1483                let temp_var_id = std::cmp::max(execution_bytecode.var_count, max_var_id + 1);
1484                execution_bytecode
1485                    .instructions
1486                    .push(runmat_ignition::Instr::StoreVar(temp_var_id));
1487                execution_bytecode.var_count = temp_var_id + 1; // Expand variable count for temp variable
1488
1489                // Ensure our variable array can hold the temporary variable
1490                if self.variable_array.len() <= temp_var_id {
1491                    self.variable_array.resize(temp_var_id + 1, Value::Num(0.0));
1492                }
1493
1494                if self.verbose {
1495                    debug!(
1496                        "Modified expression bytecode, new instructions: {:?}",
1497                        execution_bytecode.instructions
1498                    );
1499                }
1500            }
1501
1502            match self.interpret_with_context(&execution_bytecode).await {
1503                Ok(runmat_ignition::InterpreterOutcome::Completed(results)) => {
1504                    // Only increment interpreter_fallback if JIT wasn't attempted
1505                    if !self.has_jit() || is_expression_stmt {
1506                        self.stats.interpreter_fallback += 1;
1507                    }
1508                    if self.verbose {
1509                        debug!("Interpreter results: {results:?}");
1510                    }
1511
1512                    // Handle assignment statements (x = 42 should show the assigned value unless suppressed)
1513                    if hir.body.len() == 1 {
1514                        if let runmat_hir::HirStmt::Assign(var_id, _, _, _) = &hir.body[0] {
1515                            if let Some(name) = id_to_name.get(&var_id.0) {
1516                                assigned_this_execution.insert(name.clone());
1517                            }
1518                            // For assignments, capture the assigned value for both display and type info
1519                            if var_id.0 < self.variable_array.len() {
1520                                let assignment_value = self.variable_array[var_id.0].clone();
1521                                if !is_semicolon_suppressed {
1522                                    result_value = Some(assignment_value);
1523                                    if self.verbose {
1524                                        debug!("Interpreter assignment result: {result_value:?}");
1525                                    }
1526                                } else {
1527                                    suppressed_value = Some(assignment_value);
1528                                    if self.verbose {
1529                                        debug!("Interpreter assignment suppressed due to semicolon, captured for type info");
1530                                    }
1531                                }
1532                            }
1533                        } else if !is_expression_stmt
1534                            && !results.is_empty()
1535                            && !is_semicolon_suppressed
1536                            && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
1537                        {
1538                            result_value = Some(results[0].clone());
1539                        }
1540                    }
1541
1542                    // For expressions, get the result from the temporary variable (capture for both display and type info)
1543                    if is_expression_stmt
1544                        && matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)
1545                        && !execution_bytecode.instructions.is_empty()
1546                        && result_value.is_none()
1547                        && suppressed_value.is_none()
1548                    {
1549                        let temp_var_id = execution_bytecode.var_count - 1; // The temp variable we added
1550                        if temp_var_id < self.variable_array.len() {
1551                            let expression_value = self.variable_array[temp_var_id].clone();
1552                            if !is_semicolon_suppressed {
1553                                // Capture for 'ans' update when output is not suppressed
1554                                ans_update = Some((temp_var_id, expression_value.clone()));
1555                                result_value = Some(expression_value);
1556                                if self.verbose {
1557                                    debug!("Expression result from temp var {temp_var_id}: {result_value:?}");
1558                                }
1559                            } else {
1560                                suppressed_value = Some(expression_value);
1561                                if self.verbose {
1562                                    debug!("Expression suppressed, captured for type info from temp var {temp_var_id}: {suppressed_value:?}");
1563                                }
1564                            }
1565                        }
1566                    } else if !is_semicolon_suppressed
1567                        && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
1568                        && result_value.is_none()
1569                    {
1570                        result_value = results.into_iter().last();
1571                        if self.verbose {
1572                            debug!("Fallback result from interpreter: {result_value:?}");
1573                        }
1574                    }
1575
1576                    if self.verbose {
1577                        debug!("Final result_value: {result_value:?}");
1578                    }
1579                    debug!("Interpreter execution successful");
1580                }
1581
1582                Err(e) => {
1583                    debug!("Interpreter execution failed: {e}");
1584                    error = Some(e);
1585                }
1586            }
1587        }
1588
1589        let last_assign_var = last_unsuppressed_assign_var(&hir.body);
1590        let last_expr_emits = last_expr_emits_value(&hir.body);
1591        if !is_semicolon_suppressed && result_value.is_none() {
1592            if last_assign_var.is_some() || last_expr_emits {
1593                if let Some(value) = runmat_runtime::console::take_last_value_output() {
1594                    result_value = Some(value);
1595                }
1596            }
1597            if result_value.is_none() {
1598                if last_assign_var.is_some() {
1599                    if let Some(var_id) = last_emit_var_index(&bytecode) {
1600                        if var_id < self.variable_array.len() {
1601                            result_value = Some(self.variable_array[var_id].clone());
1602                        }
1603                    }
1604                }
1605                if result_value.is_none() {
1606                    if let Some(var_id) = last_assign_var {
1607                        if var_id < self.variable_array.len() {
1608                            result_value = Some(self.variable_array[var_id].clone());
1609                        }
1610                    }
1611                }
1612            }
1613        }
1614
1615        let execution_time = start_time.elapsed();
1616        let execution_time_ms = execution_time.as_millis() as u64;
1617
1618        self.stats.total_execution_time_ms += execution_time_ms;
1619        self.stats.average_execution_time_ms =
1620            self.stats.total_execution_time_ms as f64 / self.stats.total_executions as f64;
1621
1622        // Update variable names mapping and function definitions if execution was successful
1623        if error.is_none() {
1624            if let Some((mutated_names, assigned)) = runmat_ignition::take_updated_workspace_state()
1625            {
1626                if debug_trace {
1627                    debug!(
1628                        ?mutated_names,
1629                        ?assigned,
1630                        "[repl] mutated names and assigned return values"
1631                    );
1632                }
1633                self.variable_names = mutated_names.clone();
1634                let previous_workspace = self.workspace_values.clone();
1635                let current_names: HashSet<String> = assigned
1636                    .iter()
1637                    .filter(|name| {
1638                        mutated_names
1639                            .get(*name)
1640                            .map(|var_id| *var_id < self.variable_array.len())
1641                            .unwrap_or(false)
1642                    })
1643                    .cloned()
1644                    .collect();
1645                let removed_names: HashSet<String> = previous_workspace
1646                    .keys()
1647                    .filter(|name| !current_names.contains(*name))
1648                    .cloned()
1649                    .collect();
1650                let mut rebuilt_workspace = HashMap::new();
1651                let mut changed_names: HashSet<String> = assigned
1652                    .difference(&prev_assigned_snapshot)
1653                    .cloned()
1654                    .collect();
1655                changed_names.extend(assigned_this_execution.iter().cloned());
1656
1657                for name in &current_names {
1658                    let Some(var_id) = mutated_names.get(name).copied() else {
1659                        continue;
1660                    };
1661                    if var_id >= self.variable_array.len() {
1662                        continue;
1663                    }
1664                    let value_clone = self.variable_array[var_id].clone();
1665                    if previous_workspace.get(name) != Some(&value_clone) {
1666                        changed_names.insert(name.clone());
1667                    }
1668                    rebuilt_workspace.insert(name.clone(), value_clone);
1669                }
1670
1671                if debug_trace {
1672                    debug!(?changed_names, ?removed_names, "[repl] workspace changes");
1673                }
1674
1675                self.workspace_values = rebuilt_workspace;
1676                if !removed_names.is_empty() {
1677                    workspace_snapshot_force_full = true;
1678                } else {
1679                    for name in changed_names {
1680                        if let Some(value_clone) = self.workspace_values.get(&name).cloned() {
1681                            workspace_updates.push(workspace_entry(&name, &value_clone));
1682                            if debug_trace {
1683                                debug!(name, ?value_clone, "[repl] workspace update");
1684                            }
1685                        }
1686                    }
1687                }
1688            } else {
1689                for name in &assigned_this_execution {
1690                    if let Some(var_id) =
1691                        id_to_name
1692                            .iter()
1693                            .find_map(|(vid, n)| if n == name { Some(*vid) } else { None })
1694                    {
1695                        if var_id < self.variable_array.len() {
1696                            let value_clone = self.variable_array[var_id].clone();
1697                            self.workspace_values
1698                                .insert(name.clone(), value_clone.clone());
1699                            workspace_updates.push(workspace_entry(name, &value_clone));
1700                        }
1701                    }
1702                }
1703            }
1704            let mut repl_source_id: Option<SourceId> = None;
1705            for (name, stmt) in &updated_functions {
1706                if matches!(stmt, runmat_hir::HirStmt::Function { .. }) {
1707                    let source_id = *repl_source_id
1708                        .get_or_insert_with(|| self.source_pool.intern("<repl>", input));
1709                    self.function_source_ids.insert(name.clone(), source_id);
1710                }
1711            }
1712            self.function_definitions = updated_functions;
1713            // Apply 'ans' update if applicable (persisting expression result)
1714            if let Some((var_id, value)) = ans_update {
1715                self.variable_names.insert("ans".to_string(), var_id);
1716                self.workspace_values.insert("ans".to_string(), value);
1717                if debug_trace {
1718                    println!("Updated 'ans' to var_id {}", var_id);
1719                }
1720            }
1721        }
1722
1723        if self.verbose {
1724            debug!("Execution completed in {execution_time_ms}ms (JIT: {used_jit})");
1725        }
1726
1727        if !is_expression_stmt
1728            && !is_semicolon_suppressed
1729            && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
1730            && result_value.is_none()
1731        {
1732            if let Some(v) = self
1733                .variable_array
1734                .iter()
1735                .rev()
1736                .find(|v| !matches!(v, Value::Num(0.0)))
1737                .cloned()
1738            {
1739                result_value = Some(v);
1740            }
1741        }
1742
1743        if !is_semicolon_suppressed
1744            && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
1745        {
1746            if let Some(value) = result_value.as_ref() {
1747                let label = determine_display_label_from_context(
1748                    single_assign_var,
1749                    &id_to_name,
1750                    is_expression_stmt,
1751                    single_stmt_non_assign,
1752                );
1753                runmat_runtime::console::record_value_output(label.as_deref(), value);
1754            }
1755        }
1756
1757        // Generate type info if we have a suppressed value
1758        let type_info = suppressed_value.as_ref().map(format_type_info);
1759
1760        let streams = runmat_runtime::console::take_thread_buffer()
1761            .into_iter()
1762            .map(|entry| ExecutionStreamEntry {
1763                stream: match entry.stream {
1764                    runmat_runtime::console::ConsoleStream::Stdout => ExecutionStreamKind::Stdout,
1765                    runmat_runtime::console::ConsoleStream::Stderr => ExecutionStreamKind::Stderr,
1766                    runmat_runtime::console::ConsoleStream::ClearScreen => {
1767                        ExecutionStreamKind::ClearScreen
1768                    }
1769                },
1770                text: entry.text,
1771                timestamp_ms: entry.timestamp_ms,
1772            })
1773            .collect();
1774        let (workspace_entries, snapshot_full) = if workspace_snapshot_force_full {
1775            let mut entries: Vec<WorkspaceEntry> = self
1776                .workspace_values
1777                .iter()
1778                .map(|(name, value)| workspace_entry(name, value))
1779                .collect();
1780            entries.sort_by(|a, b| a.name.cmp(&b.name));
1781            (entries, true)
1782        } else if workspace_updates.is_empty() {
1783            let source_map = if self.workspace_values.is_empty() {
1784                &self.variables
1785            } else {
1786                &self.workspace_values
1787            };
1788            if source_map.is_empty() {
1789                (workspace_updates, false)
1790            } else {
1791                let mut entries: Vec<WorkspaceEntry> = source_map
1792                    .iter()
1793                    .map(|(name, value)| workspace_entry(name, value))
1794                    .collect();
1795                entries.sort_by(|a, b| a.name.cmp(&b.name));
1796                (entries, true)
1797            }
1798        } else {
1799            (workspace_updates, false)
1800        };
1801        let workspace_snapshot = self.build_workspace_snapshot(workspace_entries, snapshot_full);
1802        let figures_touched = runmat_runtime::plotting_hooks::take_recent_figures();
1803        let stdin_events = stdin_events
1804            .lock()
1805            .map(|guard| guard.clone())
1806            .unwrap_or_default();
1807
1808        let warnings = runmat_runtime::warning_store::take_all();
1809
1810        if let Some(runtime_error) = &mut error {
1811            self.normalize_error_namespace(runtime_error);
1812            self.populate_callstack(runtime_error);
1813        }
1814
1815        let suppress_public_value =
1816            is_expression_stmt && matches!(final_stmt_emit, FinalStmtEmitDisposition::Suppressed);
1817        let public_value = if is_semicolon_suppressed || suppress_public_value {
1818            None
1819        } else {
1820            result_value
1821        };
1822
1823        Ok(ExecutionResult {
1824            value: public_value,
1825            execution_time_ms,
1826            used_jit,
1827            error,
1828            type_info,
1829            streams,
1830            workspace: workspace_snapshot,
1831            figures_touched,
1832            warnings,
1833            profiling: gather_profiling(execution_time_ms),
1834            fusion_plan: fusion_snapshot,
1835            stdin_events,
1836        })
1837    }
1838
1839    /// Get execution statistics
1840    pub fn stats(&self) -> &ExecutionStats {
1841        &self.stats
1842    }
1843
1844    /// Reset execution statistics
1845    pub fn reset_stats(&mut self) {
1846        self.stats = ExecutionStats::default();
1847    }
1848
1849    /// Clear all variables in the session context
1850    pub fn clear_variables(&mut self) {
1851        self.variables.clear();
1852        self.variable_array.clear();
1853        self.variable_names.clear();
1854        self.workspace_values.clear();
1855        self.workspace_preview_tokens.clear();
1856    }
1857
1858    pub async fn export_workspace_state(
1859        &mut self,
1860        mode: WorkspaceExportMode,
1861    ) -> Result<Option<Vec<u8>>> {
1862        if matches!(mode, WorkspaceExportMode::Off) {
1863            return Ok(None);
1864        }
1865
1866        let source_map = if self.workspace_values.is_empty() {
1867            &self.variables
1868        } else {
1869            &self.workspace_values
1870        };
1871
1872        let mut entries: Vec<(String, Value)> = Vec::with_capacity(source_map.len());
1873        for (name, value) in source_map {
1874            let gathered = gather_if_needed_async(value).await?;
1875            entries.push((name.clone(), gathered));
1876        }
1877
1878        if entries.is_empty() && matches!(mode, WorkspaceExportMode::Auto) {
1879            return Ok(None);
1880        }
1881
1882        let replay_mode = match mode {
1883            WorkspaceExportMode::Auto => WorkspaceReplayMode::Auto,
1884            WorkspaceExportMode::Force => WorkspaceReplayMode::Force,
1885            WorkspaceExportMode::Off => WorkspaceReplayMode::Off,
1886        };
1887
1888        runtime_export_workspace_state(&entries, replay_mode)
1889            .await
1890            .map_err(Into::into)
1891    }
1892
1893    pub fn import_workspace_state(&mut self, bytes: &[u8]) -> Result<()> {
1894        let entries = runtime_import_workspace_state(bytes)?;
1895        self.clear_variables();
1896
1897        for (index, (name, value)) in entries.into_iter().enumerate() {
1898            self.variable_names.insert(name.clone(), index);
1899            self.variable_array.push(value.clone());
1900            self.variables.insert(name.clone(), value.clone());
1901            self.workspace_values.insert(name, value);
1902        }
1903
1904        self.workspace_preview_tokens.clear();
1905        self.workspace_version = self.workspace_version.wrapping_add(1);
1906        Ok(())
1907    }
1908
1909    pub fn workspace_snapshot(&mut self) -> WorkspaceSnapshot {
1910        let source_map = if self.workspace_values.is_empty() {
1911            &self.variables
1912        } else {
1913            &self.workspace_values
1914        };
1915
1916        let mut entries: Vec<WorkspaceEntry> = source_map
1917            .iter()
1918            .map(|(name, value)| workspace_entry(name, value))
1919            .collect();
1920        entries.sort_by(|a, b| a.name.cmp(&b.name));
1921
1922        WorkspaceSnapshot {
1923            full: true,
1924            version: self.workspace_version,
1925            values: self.attach_workspace_preview_tokens(entries),
1926        }
1927    }
1928
1929    /// Control whether fusion plan snapshots are emitted in [`ExecutionResult`].
1930    pub fn set_emit_fusion_plan(&mut self, enabled: bool) {
1931        self.emit_fusion_plan = enabled;
1932    }
1933
1934    /// Return the active language compatibility mode.
1935    pub fn compat_mode(&self) -> CompatMode {
1936        self.compat_mode
1937    }
1938
1939    /// Set the language compatibility mode (`matlab` or `strict`).
1940    pub fn set_compat_mode(&mut self, mode: CompatMode) {
1941        self.compat_mode = mode;
1942    }
1943
1944    pub fn set_callstack_limit(&mut self, limit: usize) {
1945        self.callstack_limit = limit;
1946        runmat_ignition::set_call_stack_limit(limit);
1947    }
1948
1949    pub fn set_error_namespace(&mut self, namespace: impl Into<String>) {
1950        let namespace = namespace.into();
1951        let namespace = if namespace.trim().is_empty() {
1952            runmat_ignition::DEFAULT_ERROR_NAMESPACE.to_string()
1953        } else {
1954            namespace
1955        };
1956        self.error_namespace = namespace.clone();
1957        runmat_ignition::set_error_namespace(&namespace);
1958        runmat_hir::set_error_namespace(&namespace);
1959    }
1960
1961    pub fn set_source_name_override(&mut self, name: Option<String>) {
1962        self.source_name_override = name;
1963    }
1964
1965    /// Materialize a workspace variable for inspection (optionally identified by preview token).
1966    pub async fn materialize_variable(
1967        &mut self,
1968        target: WorkspaceMaterializeTarget,
1969        options: WorkspaceMaterializeOptions,
1970    ) -> Result<MaterializedVariable> {
1971        let name = match target {
1972            WorkspaceMaterializeTarget::Name(name) => name,
1973            WorkspaceMaterializeTarget::Token(id) => self
1974                .workspace_preview_tokens
1975                .get(&id)
1976                .map(|ticket| ticket.name.clone())
1977                .ok_or_else(|| anyhow::anyhow!("Unknown workspace preview token"))?,
1978        };
1979        let value = self
1980            .workspace_values
1981            .get(&name)
1982            .or_else(|| self.variables.get(&name))
1983            .cloned()
1984            .ok_or_else(|| anyhow::anyhow!("Variable '{name}' not found in workspace"))?;
1985
1986        let is_gpu = matches!(value, Value::GpuTensor(_));
1987        let residency = if is_gpu {
1988            WorkspaceResidency::Gpu
1989        } else {
1990            WorkspaceResidency::Cpu
1991        };
1992        // For CPU values we can materialize directly. For GPU tensors, avoid downloading the
1993        // entire buffer into wasm memory; gather only the requested preview/slice.
1994        let host_value = value.clone();
1995        let value_shape_vec = value_shape(&host_value).unwrap_or_default();
1996        let mut preview = None;
1997        if is_gpu {
1998            if let Value::GpuTensor(handle) = &value {
1999                if let Some((values, truncated)) =
2000                    gather_gpu_preview_values(handle, &value_shape_vec, &options).await?
2001                {
2002                    preview = Some(WorkspacePreview { values, truncated });
2003                }
2004            }
2005        } else {
2006            if let Some(slice_opts) = options
2007                .slice
2008                .as_ref()
2009                .and_then(|slice| slice.sanitized(&value_shape_vec))
2010            {
2011                let slice_elements = slice_opts.shape.iter().product::<usize>();
2012                let slice_limit = slice_elements.clamp(1, MATERIALIZE_DEFAULT_LIMIT);
2013                if let Some(slice_value) = slice_value_for_preview(&host_value, &slice_opts) {
2014                    preview = preview_numeric_values(&slice_value, slice_limit)
2015                        .map(|(values, truncated)| WorkspacePreview { values, truncated });
2016                }
2017            }
2018            if preview.is_none() {
2019                let max_elements = options.max_elements.clamp(1, MATERIALIZE_DEFAULT_LIMIT);
2020                preview = preview_numeric_values(&host_value, max_elements)
2021                    .map(|(values, truncated)| WorkspacePreview { values, truncated });
2022            }
2023        }
2024        Ok(MaterializedVariable {
2025            name,
2026            class_name: matlab_class_name(&host_value),
2027            dtype: if let Value::GpuTensor(handle) = &host_value {
2028                gpu_dtype_label(handle).map(|label| label.to_string())
2029            } else {
2030                numeric_dtype_label(&host_value).map(|label| label.to_string())
2031            },
2032            shape: value_shape_vec,
2033            is_gpu,
2034            residency,
2035            size_bytes: if let Value::GpuTensor(handle) = &host_value {
2036                gpu_size_bytes(handle)
2037            } else {
2038                approximate_size_bytes(&host_value)
2039            },
2040            preview,
2041            value: host_value,
2042        })
2043    }
2044
2045    /// Get a copy of current variables
2046    pub fn get_variables(&self) -> &HashMap<String, Value> {
2047        &self.variables
2048    }
2049
2050    /// Interpret bytecode with persistent variable context
2051    async fn interpret_with_context(
2052        &mut self,
2053        bytecode: &runmat_ignition::Bytecode,
2054    ) -> Result<runmat_ignition::InterpreterOutcome, RuntimeError> {
2055        let source_name = self.current_source_name().to_string();
2056        runmat_ignition::interpret_with_vars(
2057            bytecode,
2058            &mut self.variable_array,
2059            Some(source_name.as_str()),
2060        )
2061        .await
2062    }
2063
2064    /// Prepare variable array for execution by populating with existing values
2065    fn prepare_variable_array_for_execution(
2066        &mut self,
2067        bytecode: &runmat_ignition::Bytecode,
2068        updated_var_mapping: &HashMap<String, usize>,
2069        debug_trace: bool,
2070    ) {
2071        // Create a new variable array of the correct size
2072        let max_var_id = updated_var_mapping.values().copied().max().unwrap_or(0);
2073        let required_len = std::cmp::max(bytecode.var_count, max_var_id + 1);
2074        let mut new_variable_array = vec![Value::Num(0.0); required_len];
2075        if debug_trace {
2076            debug!(
2077                bytecode_var_count = bytecode.var_count,
2078                required_len, max_var_id, "[repl] prepare variable array"
2079            );
2080        }
2081
2082        // Populate with existing values based on the variable mapping
2083        for (var_name, &new_var_id) in updated_var_mapping {
2084            if new_var_id < new_variable_array.len() {
2085                if let Some(value) = self.workspace_values.get(var_name) {
2086                    if debug_trace {
2087                        debug!(
2088                            var_name,
2089                            var_id = new_var_id,
2090                            ?value,
2091                            "[repl] prepare set var"
2092                        );
2093                    }
2094                    new_variable_array[new_var_id] = value.clone();
2095                }
2096            } else if debug_trace {
2097                debug!(
2098                    var_name,
2099                    var_id = new_var_id,
2100                    len = new_variable_array.len(),
2101                    "[repl] prepare skipping var"
2102                );
2103            }
2104        }
2105
2106        // Update our variable array and mapping
2107        self.variable_array = new_variable_array;
2108    }
2109
2110    /// Convert stored HIR function definitions to UserFunction format for compilation
2111    fn convert_hir_functions_to_user_functions(
2112        &self,
2113    ) -> HashMap<String, runmat_ignition::UserFunction> {
2114        let mut user_functions = HashMap::new();
2115
2116        for (name, hir_stmt) in &self.function_definitions {
2117            if let runmat_hir::HirStmt::Function {
2118                name: func_name,
2119                params,
2120                outputs,
2121                body,
2122                has_varargin: _,
2123                has_varargout: _,
2124                ..
2125            } = hir_stmt
2126            {
2127                // Use the existing HIR utilities to calculate variable count
2128                let var_map =
2129                    runmat_hir::remapping::create_complete_function_var_map(params, outputs, body);
2130                let max_local_var = var_map.len();
2131
2132                let source_id = self.function_source_ids.get(name).copied();
2133                if let Some(id) = source_id {
2134                    if let Some(source) = self.source_pool.get(id) {
2135                        let _ = (&source.name, &source.text);
2136                    }
2137                }
2138                let user_func = runmat_ignition::UserFunction {
2139                    name: func_name.clone(),
2140                    params: params.clone(),
2141                    outputs: outputs.clone(),
2142                    body: body.clone(),
2143                    local_var_count: max_local_var,
2144                    has_varargin: false,
2145                    has_varargout: false,
2146                    var_types: vec![Type::Unknown; max_local_var],
2147                    source_id,
2148                };
2149                user_functions.insert(name.clone(), user_func);
2150            }
2151        }
2152
2153        user_functions
2154    }
2155
2156    /// Configure garbage collector
2157    pub fn configure_gc(&self, config: GcConfig) -> Result<()> {
2158        gc_configure(config)
2159            .map_err(|e| anyhow::anyhow!("Failed to configure garbage collector: {}", e))
2160    }
2161
2162    /// Get GC statistics
2163    pub fn gc_stats(&self) -> runmat_gc::GcStats {
2164        gc_stats()
2165    }
2166
2167    /// Show detailed system information
2168    pub fn show_system_info(&self) {
2169        let gc_stats = self.gc_stats();
2170        info!(
2171            jit = %if self.has_jit() { "available" } else { "disabled/failed" },
2172            verbose = self.verbose,
2173            total_executions = self.stats.total_executions,
2174            jit_compiled = self.stats.jit_compiled,
2175            interpreter_fallback = self.stats.interpreter_fallback,
2176            avg_time_ms = self.stats.average_execution_time_ms,
2177            total_allocations = gc_stats
2178                .total_allocations
2179                .load(std::sync::atomic::Ordering::Relaxed),
2180            minor_collections = gc_stats
2181                .minor_collections
2182                .load(std::sync::atomic::Ordering::Relaxed),
2183            major_collections = gc_stats
2184                .major_collections
2185                .load(std::sync::atomic::Ordering::Relaxed),
2186            current_memory_mb = gc_stats
2187                .current_memory_usage
2188                .load(std::sync::atomic::Ordering::Relaxed) as f64
2189                / 1024.0
2190                / 1024.0,
2191            workspace_vars = self.workspace_values.len(),
2192            "RunMat Session Status"
2193        );
2194    }
2195
2196    #[cfg(feature = "jit")]
2197    fn has_jit(&self) -> bool {
2198        self.jit_engine.is_some()
2199    }
2200
2201    #[cfg(not(feature = "jit"))]
2202    fn has_jit(&self) -> bool {
2203        false
2204    }
2205
2206    fn build_workspace_snapshot(
2207        &mut self,
2208        entries: Vec<WorkspaceEntry>,
2209        full: bool,
2210    ) -> WorkspaceSnapshot {
2211        self.workspace_version = self.workspace_version.wrapping_add(1);
2212        let version = self.workspace_version;
2213        WorkspaceSnapshot {
2214            full,
2215            version,
2216            values: self.attach_workspace_preview_tokens(entries),
2217        }
2218    }
2219
2220    fn attach_workspace_preview_tokens(
2221        &mut self,
2222        entries: Vec<WorkspaceEntry>,
2223    ) -> Vec<WorkspaceEntry> {
2224        self.workspace_preview_tokens.clear();
2225        let mut values = Vec::with_capacity(entries.len());
2226        for mut entry in entries {
2227            let token = Uuid::new_v4();
2228            self.workspace_preview_tokens.insert(
2229                token,
2230                WorkspaceMaterializeTicket {
2231                    name: entry.name.clone(),
2232                },
2233            );
2234            entry.preview_token = Some(token);
2235            values.push(entry);
2236        }
2237        values
2238    }
2239}
2240
2241fn last_displayable_statement_emit_disposition(
2242    body: &[runmat_hir::HirStmt],
2243) -> FinalStmtEmitDisposition {
2244    use runmat_hir::HirStmt;
2245
2246    for stmt in body.iter().rev() {
2247        match stmt {
2248            HirStmt::ExprStmt(expr, _, _) => return expr_emit_disposition(expr),
2249            HirStmt::Assign(_, _, _, _) | HirStmt::MultiAssign(_, _, _, _) => {
2250                return FinalStmtEmitDisposition::Suppressed
2251            }
2252            HirStmt::AssignLValue(_, _, _, _) => return FinalStmtEmitDisposition::Suppressed,
2253
2254            _ => continue,
2255        }
2256    }
2257    FinalStmtEmitDisposition::Suppressed
2258}
2259
2260fn last_unsuppressed_assign_var(body: &[runmat_hir::HirStmt]) -> Option<usize> {
2261    use runmat_hir::HirStmt;
2262
2263    for stmt in body.iter().rev() {
2264        match stmt {
2265            HirStmt::Assign(var_id, _, suppressed, _) => {
2266                return if *suppressed { None } else { Some(var_id.0) };
2267            }
2268            HirStmt::ExprStmt(_, _, _)
2269            | HirStmt::MultiAssign(_, _, _, _)
2270            | HirStmt::AssignLValue(_, _, _, _) => return None,
2271            _ => continue,
2272        }
2273    }
2274    None
2275}
2276
2277fn last_expr_emits_value(body: &[runmat_hir::HirStmt]) -> bool {
2278    use runmat_hir::HirStmt;
2279
2280    for stmt in body.iter().rev() {
2281        match stmt {
2282            HirStmt::ExprStmt(expr, suppressed, _) => {
2283                if *suppressed {
2284                    return false;
2285                }
2286                return matches!(
2287                    expr_emit_disposition(expr),
2288                    FinalStmtEmitDisposition::Inline
2289                );
2290            }
2291            HirStmt::Assign(_, _, _, _)
2292            | HirStmt::MultiAssign(_, _, _, _)
2293            | HirStmt::AssignLValue(_, _, _, _) => return false,
2294            _ => continue,
2295        }
2296    }
2297    false
2298}
2299
2300fn last_emit_var_index(bytecode: &runmat_ignition::Bytecode) -> Option<usize> {
2301    for instr in bytecode.instructions.iter().rev() {
2302        if let runmat_ignition::Instr::EmitVar { var_index, .. } = instr {
2303            return Some(*var_index);
2304        }
2305    }
2306    None
2307}
2308
2309fn expr_emit_disposition(expr: &runmat_hir::HirExpr) -> FinalStmtEmitDisposition {
2310    use runmat_hir::HirExprKind;
2311    if let HirExprKind::FuncCall(name, _) = &expr.kind {
2312        if runmat_builtins::suppresses_auto_output(name) {
2313            return FinalStmtEmitDisposition::Suppressed;
2314        }
2315    }
2316    FinalStmtEmitDisposition::Inline
2317}
2318
2319const WORKSPACE_PREVIEW_LIMIT: usize = 16;
2320const MATERIALIZE_DEFAULT_LIMIT: usize = 4096;
2321
2322fn workspace_entry(name: &str, value: &Value) -> WorkspaceEntry {
2323    let dtype = numeric_dtype_label(value).map(|label| label.to_string());
2324    let preview = preview_numeric_values(value, WORKSPACE_PREVIEW_LIMIT)
2325        .map(|(values, truncated)| WorkspacePreview { values, truncated });
2326    let residency = if matches!(value, Value::GpuTensor(_)) {
2327        WorkspaceResidency::Gpu
2328    } else {
2329        WorkspaceResidency::Cpu
2330    };
2331    WorkspaceEntry {
2332        name: name.to_string(),
2333        class_name: matlab_class_name(value),
2334        dtype,
2335        shape: value_shape(value).unwrap_or_default(),
2336        is_gpu: matches!(value, Value::GpuTensor(_)),
2337        size_bytes: approximate_size_bytes(value),
2338        preview,
2339        residency,
2340        preview_token: None,
2341    }
2342}
2343
2344struct ActiveExecutionGuard {
2345    flag: *mut bool,
2346}
2347
2348impl ActiveExecutionGuard {
2349    fn new(session: &mut RunMatSession) -> Result<Self> {
2350        if session.is_executing {
2351            Err(anyhow::anyhow!(
2352                "RunMatSession is already executing another script"
2353            ))
2354        } else {
2355            session.is_executing = true;
2356            Ok(Self {
2357                flag: &mut session.is_executing,
2358            })
2359        }
2360    }
2361}
2362
2363impl Drop for ActiveExecutionGuard {
2364    fn drop(&mut self) {
2365        unsafe {
2366            if let Some(flag) = self.flag.as_mut() {
2367                *flag = false;
2368            }
2369        }
2370    }
2371}
2372
2373impl Default for RunMatSession {
2374    fn default() -> Self {
2375        Self::new().expect("Failed to create default RunMat session")
2376    }
2377}
2378
2379/// Tokenize the input string and return a space separated string of token names.
2380/// This is kept for backward compatibility with existing tests.
2381pub fn format_tokens(input: &str) -> String {
2382    tokenize_detailed(input)
2383        .into_iter()
2384        .map(|t| format!("{:?}", t.token))
2385        .collect::<Vec<_>>()
2386        .join(" ")
2387}
2388
2389/// Execute MATLAB/Octave code and return the result as a formatted string
2390pub async fn execute_and_format(input: &str) -> String {
2391    match RunMatSession::new() {
2392        Ok(mut engine) => match engine.execute(input).await {
2393            Ok(result) => {
2394                if let Some(error) = result.error {
2395                    format!("Error: {error}")
2396                } else if let Some(value) = result.value {
2397                    format!("{value:?}")
2398                } else {
2399                    "".to_string()
2400                }
2401            }
2402            Err(e) => format!("Error: {e}"),
2403        },
2404        Err(e) => format!("Engine Error: {e}"),
2405    }
2406}
2407
2408#[cfg(not(target_arch = "wasm32"))]
2409fn reset_provider_telemetry() {
2410    if let Some(provider) = accel_provider() {
2411        provider.reset_telemetry();
2412    }
2413}
2414
2415#[cfg(target_arch = "wasm32")]
2416fn reset_provider_telemetry() {}
2417
2418#[cfg(not(target_arch = "wasm32"))]
2419fn gather_profiling(execution_time_ms: u64) -> Option<ExecutionProfiling> {
2420    let provider = accel_provider()?;
2421    let telemetry = provider.telemetry_snapshot();
2422    let gpu_ns = telemetry.fused_elementwise.total_wall_time_ns
2423        + telemetry.fused_reduction.total_wall_time_ns
2424        + telemetry.matmul.total_wall_time_ns;
2425    let gpu_ms = gpu_ns.saturating_div(1_000_000);
2426    Some(ExecutionProfiling {
2427        total_ms: execution_time_ms,
2428        cpu_ms: Some(execution_time_ms.saturating_sub(gpu_ms)),
2429        gpu_ms: Some(gpu_ms),
2430        kernel_count: Some(
2431            (telemetry.fused_elementwise.count
2432                + telemetry.fused_reduction.count
2433                + telemetry.matmul.count
2434                + telemetry.kernel_launches.len() as u64)
2435                .min(u32::MAX as u64) as u32,
2436        ),
2437    })
2438}
2439
2440#[cfg(target_arch = "wasm32")]
2441fn gather_profiling(execution_time_ms: u64) -> Option<ExecutionProfiling> {
2442    Some(ExecutionProfiling {
2443        total_ms: execution_time_ms,
2444        ..ExecutionProfiling::default()
2445    })
2446}
2447
2448#[cfg(test)]
2449mod tests {
2450    use super::*;
2451    use futures::executor::block_on;
2452
2453    #[test]
2454    fn captures_basic_workspace_assignments() {
2455        let mut session =
2456            RunMatSession::with_snapshot_bytes(false, false, None).expect("session init");
2457        let result = block_on(session.execute("x = 42;")).expect("exec succeeds");
2458        assert!(
2459            result
2460                .workspace
2461                .values
2462                .iter()
2463                .any(|entry| entry.name == "x"),
2464            "workspace snapshot should include assigned variable"
2465        );
2466    }
2467
2468    #[test]
2469    fn workspace_reports_datetime_array_shape() {
2470        let mut session =
2471            RunMatSession::with_snapshot_bytes(false, false, None).expect("session init");
2472        let result =
2473            block_on(session.execute("d = datetime([739351; 739352], 'ConvertFrom', 'datenum');"))
2474                .expect("exec succeeds");
2475        let entry = result
2476            .workspace
2477            .values
2478            .iter()
2479            .find(|entry| entry.name == "d")
2480            .expect("workspace entry for d");
2481        assert_eq!(entry.class_name, "datetime");
2482        assert_eq!(entry.shape, vec![2, 1]);
2483    }
2484
2485    #[test]
2486    fn workspace_state_roundtrip_replace_only() {
2487        let mut source_session =
2488            RunMatSession::with_snapshot_bytes(false, false, None).expect("session init");
2489        let _ = block_on(source_session.execute("x = 42; y = [1, 2, 3];")).expect("exec succeeds");
2490
2491        let bytes = block_on(source_session.export_workspace_state(WorkspaceExportMode::Force))
2492            .expect("workspace export")
2493            .expect("workspace bytes");
2494
2495        let mut restore_session =
2496            RunMatSession::with_snapshot_bytes(false, false, None).expect("session init");
2497        let _ = block_on(restore_session.execute("z = 99;")).expect("seed workspace");
2498        restore_session
2499            .import_workspace_state(&bytes)
2500            .expect("workspace import");
2501
2502        let _restored =
2503            block_on(restore_session.execute("r = x + y(2);")).expect("post-import exec");
2504
2505        let x = block_on(restore_session.materialize_variable(
2506            WorkspaceMaterializeTarget::Name("x".to_string()),
2507            WorkspaceMaterializeOptions::default(),
2508        ))
2509        .expect("x should exist after import");
2510        assert_eq!(x.name, "x");
2511
2512        let y = block_on(restore_session.materialize_variable(
2513            WorkspaceMaterializeTarget::Name("y".to_string()),
2514            WorkspaceMaterializeOptions::default(),
2515        ))
2516        .expect("y should exist after import");
2517        assert_eq!(y.name, "y");
2518
2519        let z = block_on(restore_session.materialize_variable(
2520            WorkspaceMaterializeTarget::Name("z".to_string()),
2521            WorkspaceMaterializeOptions::default(),
2522        ));
2523        assert!(
2524            z.is_err(),
2525            "replace-only import should drop stale z variable"
2526        );
2527    }
2528
2529    #[test]
2530    fn workspace_state_import_rejects_invalid_payload() {
2531        let mut session =
2532            RunMatSession::with_snapshot_bytes(false, false, None).expect("session init");
2533        let err = session
2534            .import_workspace_state(&[1, 2, 3, 4])
2535            .expect_err("invalid payload should be rejected");
2536        let runtime_err = err
2537            .downcast_ref::<runmat_runtime::RuntimeError>()
2538            .expect("error should preserve runtime replay details");
2539        assert_eq!(
2540            runtime_err.identifier(),
2541            Some("RunMat:ReplayDecodeFailed"),
2542            "invalid payload should map to replay decode identifier"
2543        );
2544    }
2545}