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        // Install a stateless expression evaluator for `input()` numeric parsing.
1254        //
1255        // The hook runs the full parse → lower → compile → interpret pipeline so
1256        // that users can type arbitrary MATLAB expressions at an input() prompt:
1257        // `sqrt(2)`, `pi/2`, `ones(3)`, `[1 2; 3 4]`, etc.
1258        //
1259        // Stack-overflow hazard: the hook calls runmat_ignition::interpret() while
1260        // the outer interpret() is already on the call stack. On WASM the JS event
1261        // loop drives both as async state-machines and the WASM linear stack is
1262        // large, so nesting is safe. On native the default thread stack is too
1263        // small for two nested interpret() invocations, so we instead run the inner
1264        // interpret() on a dedicated thread that has its own 16 MB stack and block
1265        // the calling future synchronously on the result (safe because the native
1266        // executor — futures::executor::block_on — is already synchronous).
1267        let compat = self.compat_mode;
1268        let _eval_hook_guard =
1269            runmat_runtime::interaction::replace_eval_hook(Some(std::sync::Arc::new(
1270                move |expr: String| -> runmat_runtime::interaction::EvalHookFuture {
1271                    // Shared eval logic, used by both the WASM async path and the
1272                    // native thread path below.
1273                    async fn eval_expr(
1274                        expr: String,
1275                        compat: runmat_parser::CompatMode,
1276                    ) -> Result<Value, RuntimeError> {
1277                        let wrapped = format!("__runmat_input_result__ = ({expr});");
1278                        let ast = parse_with_options(&wrapped, ParserOptions::new(compat))
1279                            .map_err(|e| {
1280                                build_runtime_error(format!("input: parse error: {e}"))
1281                                    .with_identifier("RunMat:input:ParseError")
1282                                    .build()
1283                            })?;
1284                        let lowering = runmat_hir::lower(
1285                            &ast,
1286                            &LoweringContext::new(&HashMap::new(), &HashMap::new()),
1287                        )
1288                        .map_err(|e| {
1289                            build_runtime_error(format!("input: lowering error: {e}"))
1290                                .with_identifier("RunMat:input:LowerError")
1291                                .build()
1292                        })?;
1293                        let result_idx = lowering.variables.get("__runmat_input_result__").copied();
1294                        let bc = runmat_ignition::compile(&lowering.hir, &HashMap::new())
1295                            .map_err(RuntimeError::from)?;
1296                        let vars = runmat_ignition::interpret(&bc).await?;
1297                        result_idx
1298                            .and_then(|idx| vars.get(idx).cloned())
1299                            .ok_or_else(|| {
1300                                build_runtime_error("input: expression produced no value")
1301                                    .with_identifier("RunMat:input:NoValue")
1302                                    .build()
1303                            })
1304                    }
1305
1306                    #[cfg(target_arch = "wasm32")]
1307                    {
1308                        // On WASM: await the inner interpret() directly. The JS async
1309                        // runtime handles both futures as cooperative state-machines and
1310                        // the WASM linear stack is large enough for the extra frames.
1311                        Box::pin(eval_expr(expr, compat))
1312                    }
1313
1314                    #[cfg(not(target_arch = "wasm32"))]
1315                    {
1316                        // On native: run interpret() on a dedicated thread so it gets
1317                        // its own 16 MB stack, fully isolated from the outer interpret()
1318                        // call stack. The result is sent back via a tokio oneshot channel
1319                        // and awaited asynchronously so the tokio worker thread is never
1320                        // blocked by a synchronous recv().
1321                        let (tx, rx) = tokio::sync::oneshot::channel();
1322                        let spawn_result = std::thread::Builder::new()
1323                            .stack_size(16 * 1024 * 1024)
1324                            .spawn(move || {
1325                                let result = futures::executor::block_on(eval_expr(expr, compat));
1326                                let _ = tx.send(result);
1327                            });
1328                        Box::pin(async move {
1329                            spawn_result.map_err(|err| {
1330                                build_runtime_error(format!(
1331                                    "input: failed to spawn eval thread: {err}"
1332                                ))
1333                                .with_identifier("RunMat:input:EvalThreadSpawnFailed")
1334                                .build()
1335                            })?;
1336                            rx.await.unwrap_or_else(|_| {
1337                                Err(build_runtime_error("input: eval thread panicked")
1338                                    .with_identifier("RunMat:input:EvalThreadPanic")
1339                                    .build())
1340                            })
1341                        })
1342                    }
1343                },
1344            )));
1345
1346        if self.verbose {
1347            debug!("Executing: {}", input.trim());
1348        }
1349
1350        let _source_guard = runmat_runtime::source_context::replace_current_source(Some(input));
1351
1352        let PreparedExecution {
1353            ast,
1354            lowering,
1355            mut bytecode,
1356        } = self.compile_input(input)?;
1357        if self.verbose {
1358            debug!("AST: {ast:?}");
1359        }
1360        let (hir, updated_vars, updated_functions, var_names_map) = (
1361            lowering.hir,
1362            lowering.variables,
1363            lowering.functions,
1364            lowering.var_names,
1365        );
1366        let max_var_id = updated_vars.values().copied().max().unwrap_or(0);
1367        if debug_trace {
1368            debug!(?updated_vars, "[repl] updated_vars");
1369        }
1370        if debug_trace {
1371            debug!(workspace_values_before = ?self.workspace_values, "[repl] workspace snapshot before execution");
1372        }
1373        let id_to_name: HashMap<usize, String> = var_names_map
1374            .iter()
1375            .map(|(var_id, name)| (var_id.0, name.clone()))
1376            .collect();
1377        let mut assigned_this_execution: HashSet<String> = HashSet::new();
1378        let assigned_snapshot: HashSet<String> = updated_vars
1379            .keys()
1380            .filter(|name| self.workspace_values.contains_key(name.as_str()))
1381            .cloned()
1382            .collect();
1383        let prev_assigned_snapshot = assigned_snapshot.clone();
1384        if debug_trace {
1385            debug!(?assigned_snapshot, "[repl] assigned snapshot");
1386        }
1387        let _pending_workspace_guard = runmat_ignition::push_pending_workspace(
1388            updated_vars.clone(),
1389            assigned_snapshot.clone(),
1390        );
1391        if self.verbose {
1392            debug!("HIR generated successfully");
1393        }
1394
1395        let (single_assign_var, single_stmt_non_assign) = if hir.body.len() == 1 {
1396            match &hir.body[0] {
1397                runmat_hir::HirStmt::Assign(var_id, _, _, _) => (Some(var_id.0), false),
1398                _ => (None, true),
1399            }
1400        } else {
1401            (None, false)
1402        };
1403
1404        bytecode.var_names = id_to_name.clone();
1405        if self.verbose {
1406            debug!(
1407                "Bytecode compiled: {} instructions",
1408                bytecode.instructions.len()
1409            );
1410        }
1411
1412        #[cfg(not(target_arch = "wasm32"))]
1413        let fusion_snapshot = if self.emit_fusion_plan {
1414            build_fusion_snapshot(bytecode.accel_graph.as_ref(), &bytecode.fusion_groups)
1415        } else {
1416            None
1417        };
1418        #[cfg(target_arch = "wasm32")]
1419        let fusion_snapshot: Option<FusionPlanSnapshot> = None;
1420
1421        // Prepare variable array with existing values before execution
1422        self.prepare_variable_array_for_execution(&bytecode, &updated_vars, debug_trace);
1423
1424        if self.verbose {
1425            debug!(
1426                "Variable array after preparation: {:?}",
1427                self.variable_array
1428            );
1429            debug!("Updated variable mapping: {updated_vars:?}");
1430            debug!("Bytecode instructions: {:?}", bytecode.instructions);
1431        }
1432
1433        #[cfg(feature = "jit")]
1434        let mut used_jit = false;
1435        #[cfg(not(feature = "jit"))]
1436        let used_jit = false;
1437        #[cfg(feature = "jit")]
1438        let mut execution_completed = false;
1439        #[cfg(not(feature = "jit"))]
1440        let execution_completed = false;
1441        let mut result_value: Option<Value> = None; // Always start fresh for each execution
1442        let mut suppressed_value: Option<Value> = None; // Track value for type info when suppressed
1443        let mut error = None;
1444        let mut workspace_updates: Vec<WorkspaceEntry> = Vec::new();
1445        let mut workspace_snapshot_force_full = false;
1446        let mut ans_update: Option<(usize, Value)> = None;
1447
1448        // Check if this is an expression statement (ends with Pop)
1449        let is_expression_stmt = bytecode
1450            .instructions
1451            .last()
1452            .map(|instr| matches!(instr, runmat_ignition::Instr::Pop))
1453            .unwrap_or(false);
1454
1455        // Determine whether the final statement ended with a semicolon by inspecting the raw input.
1456        let is_semicolon_suppressed = {
1457            let toks = tokenize_detailed(input);
1458            toks.into_iter()
1459                .rev()
1460                .map(|t| t.token)
1461                .find(|token| {
1462                    !matches!(
1463                        token,
1464                        LexToken::Newline
1465                            | LexToken::LineComment
1466                            | LexToken::BlockComment
1467                            | LexToken::Section
1468                    )
1469                })
1470                .map(|t| matches!(t, LexToken::Semicolon))
1471                .unwrap_or(false)
1472        };
1473        let final_stmt_emit = last_displayable_statement_emit_disposition(&hir.body);
1474
1475        if self.verbose {
1476            debug!("HIR body len: {}", hir.body.len());
1477            if !hir.body.is_empty() {
1478                debug!("HIR statement: {:?}", &hir.body[0]);
1479            }
1480            debug!("is_semicolon_suppressed: {is_semicolon_suppressed}");
1481        }
1482
1483        // Use JIT for assignments, interpreter for expressions (to capture results properly)
1484        #[cfg(feature = "jit")]
1485        {
1486            if let Some(ref mut jit_engine) = &mut self.jit_engine {
1487                if !is_expression_stmt {
1488                    // Ensure variable array is large enough
1489                    if self.variable_array.len() < bytecode.var_count {
1490                        self.variable_array
1491                            .resize(bytecode.var_count, Value::Num(0.0));
1492                    }
1493
1494                    if self.verbose {
1495                        debug!(
1496                            "JIT path for assignment: variable_array size: {}, bytecode.var_count: {}",
1497                            self.variable_array.len(),
1498                            bytecode.var_count
1499                        );
1500                    }
1501
1502                    // Use JIT for assignments
1503                    match jit_engine.execute_or_compile(&bytecode, &mut self.variable_array) {
1504                        Ok((_, actual_used_jit)) => {
1505                            used_jit = actual_used_jit;
1506                            execution_completed = true;
1507                            if actual_used_jit {
1508                                self.stats.jit_compiled += 1;
1509                            } else {
1510                                self.stats.interpreter_fallback += 1;
1511                            }
1512                            if let Some(runmat_hir::HirStmt::Assign(var_id, _, _, _)) =
1513                                hir.body.first()
1514                            {
1515                                if let Some(name) = id_to_name.get(&var_id.0) {
1516                                    assigned_this_execution.insert(name.clone());
1517                                }
1518                                if var_id.0 < self.variable_array.len() {
1519                                    let assignment_value = self.variable_array[var_id.0].clone();
1520                                    if !is_semicolon_suppressed {
1521                                        result_value = Some(assignment_value);
1522                                        if self.verbose {
1523                                            debug!("JIT assignment result: {result_value:?}");
1524                                        }
1525                                    } else {
1526                                        suppressed_value = Some(assignment_value);
1527                                        if self.verbose {
1528                                            debug!("JIT assignment suppressed due to semicolon, captured for type info");
1529                                        }
1530                                    }
1531                                }
1532                            }
1533
1534                            if self.verbose {
1535                                debug!(
1536                                    "{} assignment successful, variable_array: {:?}",
1537                                    if actual_used_jit {
1538                                        "JIT"
1539                                    } else {
1540                                        "Interpreter"
1541                                    },
1542                                    self.variable_array
1543                                );
1544                            }
1545                        }
1546                        Err(e) => {
1547                            if self.verbose {
1548                                debug!("JIT execution failed: {e}, using interpreter");
1549                            }
1550                            // Fall back to interpreter
1551                        }
1552                    }
1553                }
1554            }
1555        }
1556
1557        // Use interpreter if JIT failed or is disabled
1558        if !execution_completed {
1559            if self.verbose {
1560                debug!(
1561                    "Interpreter path: variable_array size: {}, bytecode.var_count: {}",
1562                    self.variable_array.len(),
1563                    bytecode.var_count
1564                );
1565            }
1566
1567            // For expressions, modify bytecode to store result in a temp variable instead of using stack
1568            let mut execution_bytecode = bytecode.clone();
1569            if is_expression_stmt
1570                && matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)
1571                && !execution_bytecode.instructions.is_empty()
1572            {
1573                execution_bytecode.instructions.pop(); // Remove the Pop instruction
1574
1575                // Add StoreVar instruction to store the result in a temporary variable
1576                let temp_var_id = std::cmp::max(execution_bytecode.var_count, max_var_id + 1);
1577                execution_bytecode
1578                    .instructions
1579                    .push(runmat_ignition::Instr::StoreVar(temp_var_id));
1580                execution_bytecode.var_count = temp_var_id + 1; // Expand variable count for temp variable
1581
1582                // Ensure our variable array can hold the temporary variable
1583                if self.variable_array.len() <= temp_var_id {
1584                    self.variable_array.resize(temp_var_id + 1, Value::Num(0.0));
1585                }
1586
1587                if self.verbose {
1588                    debug!(
1589                        "Modified expression bytecode, new instructions: {:?}",
1590                        execution_bytecode.instructions
1591                    );
1592                }
1593            }
1594
1595            match self.interpret_with_context(&execution_bytecode).await {
1596                Ok(runmat_ignition::InterpreterOutcome::Completed(results)) => {
1597                    // Only increment interpreter_fallback if JIT wasn't attempted
1598                    if !self.has_jit() || is_expression_stmt {
1599                        self.stats.interpreter_fallback += 1;
1600                    }
1601                    if self.verbose {
1602                        debug!("Interpreter results: {results:?}");
1603                    }
1604
1605                    // Handle assignment statements (x = 42 should show the assigned value unless suppressed)
1606                    if hir.body.len() == 1 {
1607                        if let runmat_hir::HirStmt::Assign(var_id, _, _, _) = &hir.body[0] {
1608                            if let Some(name) = id_to_name.get(&var_id.0) {
1609                                assigned_this_execution.insert(name.clone());
1610                            }
1611                            // For assignments, capture the assigned value for both display and type info
1612                            if var_id.0 < self.variable_array.len() {
1613                                let assignment_value = self.variable_array[var_id.0].clone();
1614                                if !is_semicolon_suppressed {
1615                                    result_value = Some(assignment_value);
1616                                    if self.verbose {
1617                                        debug!("Interpreter assignment result: {result_value:?}");
1618                                    }
1619                                } else {
1620                                    suppressed_value = Some(assignment_value);
1621                                    if self.verbose {
1622                                        debug!("Interpreter assignment suppressed due to semicolon, captured for type info");
1623                                    }
1624                                }
1625                            }
1626                        } else if !is_expression_stmt
1627                            && !results.is_empty()
1628                            && !is_semicolon_suppressed
1629                            && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
1630                        {
1631                            result_value = Some(results[0].clone());
1632                        }
1633                    }
1634
1635                    // For expressions, get the result from the temporary variable (capture for both display and type info)
1636                    if is_expression_stmt
1637                        && matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)
1638                        && !execution_bytecode.instructions.is_empty()
1639                        && result_value.is_none()
1640                        && suppressed_value.is_none()
1641                    {
1642                        let temp_var_id = execution_bytecode.var_count - 1; // The temp variable we added
1643                        if temp_var_id < self.variable_array.len() {
1644                            let expression_value = self.variable_array[temp_var_id].clone();
1645                            if !is_semicolon_suppressed {
1646                                // Capture for 'ans' update when output is not suppressed
1647                                ans_update = Some((temp_var_id, expression_value.clone()));
1648                                result_value = Some(expression_value);
1649                                if self.verbose {
1650                                    debug!("Expression result from temp var {temp_var_id}: {result_value:?}");
1651                                }
1652                            } else {
1653                                suppressed_value = Some(expression_value);
1654                                if self.verbose {
1655                                    debug!("Expression suppressed, captured for type info from temp var {temp_var_id}: {suppressed_value:?}");
1656                                }
1657                            }
1658                        }
1659                    } else if !is_semicolon_suppressed
1660                        && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
1661                        && result_value.is_none()
1662                    {
1663                        result_value = results.into_iter().last();
1664                        if self.verbose {
1665                            debug!("Fallback result from interpreter: {result_value:?}");
1666                        }
1667                    }
1668
1669                    if self.verbose {
1670                        debug!("Final result_value: {result_value:?}");
1671                    }
1672                    debug!("Interpreter execution successful");
1673                }
1674
1675                Err(e) => {
1676                    debug!("Interpreter execution failed: {e}");
1677                    error = Some(e);
1678                }
1679            }
1680        }
1681
1682        let last_assign_var = last_unsuppressed_assign_var(&hir.body);
1683        let last_expr_emits = last_expr_emits_value(&hir.body);
1684        if !is_semicolon_suppressed && result_value.is_none() {
1685            if last_assign_var.is_some() || last_expr_emits {
1686                if let Some(value) = runmat_runtime::console::take_last_value_output() {
1687                    result_value = Some(value);
1688                }
1689            }
1690            if result_value.is_none() {
1691                if last_assign_var.is_some() {
1692                    if let Some(var_id) = last_emit_var_index(&bytecode) {
1693                        if var_id < self.variable_array.len() {
1694                            result_value = Some(self.variable_array[var_id].clone());
1695                        }
1696                    }
1697                }
1698                if result_value.is_none() {
1699                    if let Some(var_id) = last_assign_var {
1700                        if var_id < self.variable_array.len() {
1701                            result_value = Some(self.variable_array[var_id].clone());
1702                        }
1703                    }
1704                }
1705            }
1706        }
1707
1708        let execution_time = start_time.elapsed();
1709        let execution_time_ms = execution_time.as_millis() as u64;
1710
1711        self.stats.total_execution_time_ms += execution_time_ms;
1712        self.stats.average_execution_time_ms =
1713            self.stats.total_execution_time_ms as f64 / self.stats.total_executions as f64;
1714
1715        // Update variable names mapping and function definitions if execution was successful
1716        if error.is_none() {
1717            if let Some((mutated_names, assigned)) = runmat_ignition::take_updated_workspace_state()
1718            {
1719                if debug_trace {
1720                    debug!(
1721                        ?mutated_names,
1722                        ?assigned,
1723                        "[repl] mutated names and assigned return values"
1724                    );
1725                }
1726                self.variable_names = mutated_names.clone();
1727                let previous_workspace = self.workspace_values.clone();
1728                let current_names: HashSet<String> = assigned
1729                    .iter()
1730                    .filter(|name| {
1731                        mutated_names
1732                            .get(*name)
1733                            .map(|var_id| *var_id < self.variable_array.len())
1734                            .unwrap_or(false)
1735                    })
1736                    .cloned()
1737                    .collect();
1738                let removed_names: HashSet<String> = previous_workspace
1739                    .keys()
1740                    .filter(|name| !current_names.contains(*name))
1741                    .cloned()
1742                    .collect();
1743                let mut rebuilt_workspace = HashMap::new();
1744                let mut changed_names: HashSet<String> = assigned
1745                    .difference(&prev_assigned_snapshot)
1746                    .cloned()
1747                    .collect();
1748                changed_names.extend(assigned_this_execution.iter().cloned());
1749
1750                for name in &current_names {
1751                    let Some(var_id) = mutated_names.get(name).copied() else {
1752                        continue;
1753                    };
1754                    if var_id >= self.variable_array.len() {
1755                        continue;
1756                    }
1757                    let value_clone = self.variable_array[var_id].clone();
1758                    if previous_workspace.get(name) != Some(&value_clone) {
1759                        changed_names.insert(name.clone());
1760                    }
1761                    rebuilt_workspace.insert(name.clone(), value_clone);
1762                }
1763
1764                if debug_trace {
1765                    debug!(?changed_names, ?removed_names, "[repl] workspace changes");
1766                }
1767
1768                self.workspace_values = rebuilt_workspace;
1769                if !removed_names.is_empty() {
1770                    workspace_snapshot_force_full = true;
1771                } else {
1772                    for name in changed_names {
1773                        if let Some(value_clone) = self.workspace_values.get(&name).cloned() {
1774                            workspace_updates.push(workspace_entry(&name, &value_clone));
1775                            if debug_trace {
1776                                debug!(name, ?value_clone, "[repl] workspace update");
1777                            }
1778                        }
1779                    }
1780                }
1781            } else {
1782                for name in &assigned_this_execution {
1783                    if let Some(var_id) =
1784                        id_to_name
1785                            .iter()
1786                            .find_map(|(vid, n)| if n == name { Some(*vid) } else { None })
1787                    {
1788                        if var_id < self.variable_array.len() {
1789                            let value_clone = self.variable_array[var_id].clone();
1790                            self.workspace_values
1791                                .insert(name.clone(), value_clone.clone());
1792                            workspace_updates.push(workspace_entry(name, &value_clone));
1793                        }
1794                    }
1795                }
1796            }
1797            let mut repl_source_id: Option<SourceId> = None;
1798            for (name, stmt) in &updated_functions {
1799                if matches!(stmt, runmat_hir::HirStmt::Function { .. }) {
1800                    let source_id = *repl_source_id
1801                        .get_or_insert_with(|| self.source_pool.intern("<repl>", input));
1802                    self.function_source_ids.insert(name.clone(), source_id);
1803                }
1804            }
1805            self.function_definitions = updated_functions;
1806            // Apply 'ans' update if applicable (persisting expression result)
1807            if let Some((var_id, value)) = ans_update {
1808                self.variable_names.insert("ans".to_string(), var_id);
1809                self.workspace_values.insert("ans".to_string(), value);
1810                if debug_trace {
1811                    println!("Updated 'ans' to var_id {}", var_id);
1812                }
1813            }
1814        }
1815
1816        if self.verbose {
1817            debug!("Execution completed in {execution_time_ms}ms (JIT: {used_jit})");
1818        }
1819
1820        if !is_expression_stmt
1821            && !is_semicolon_suppressed
1822            && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
1823            && result_value.is_none()
1824        {
1825            if let Some(v) = self
1826                .variable_array
1827                .iter()
1828                .rev()
1829                .find(|v| !matches!(v, Value::Num(0.0)))
1830                .cloned()
1831            {
1832                result_value = Some(v);
1833            }
1834        }
1835
1836        if !is_semicolon_suppressed
1837            && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
1838        {
1839            if let Some(value) = result_value.as_ref() {
1840                let label = determine_display_label_from_context(
1841                    single_assign_var,
1842                    &id_to_name,
1843                    is_expression_stmt,
1844                    single_stmt_non_assign,
1845                );
1846                runmat_runtime::console::record_value_output(label.as_deref(), value);
1847            }
1848        }
1849
1850        // Generate type info if we have a suppressed value
1851        let type_info = suppressed_value.as_ref().map(format_type_info);
1852
1853        let streams = runmat_runtime::console::take_thread_buffer()
1854            .into_iter()
1855            .map(|entry| ExecutionStreamEntry {
1856                stream: match entry.stream {
1857                    runmat_runtime::console::ConsoleStream::Stdout => ExecutionStreamKind::Stdout,
1858                    runmat_runtime::console::ConsoleStream::Stderr => ExecutionStreamKind::Stderr,
1859                    runmat_runtime::console::ConsoleStream::ClearScreen => {
1860                        ExecutionStreamKind::ClearScreen
1861                    }
1862                },
1863                text: entry.text,
1864                timestamp_ms: entry.timestamp_ms,
1865            })
1866            .collect();
1867        let (workspace_entries, snapshot_full) = if workspace_snapshot_force_full {
1868            let mut entries: Vec<WorkspaceEntry> = self
1869                .workspace_values
1870                .iter()
1871                .map(|(name, value)| workspace_entry(name, value))
1872                .collect();
1873            entries.sort_by(|a, b| a.name.cmp(&b.name));
1874            (entries, true)
1875        } else if workspace_updates.is_empty() {
1876            let source_map = if self.workspace_values.is_empty() {
1877                &self.variables
1878            } else {
1879                &self.workspace_values
1880            };
1881            if source_map.is_empty() {
1882                (workspace_updates, false)
1883            } else {
1884                let mut entries: Vec<WorkspaceEntry> = source_map
1885                    .iter()
1886                    .map(|(name, value)| workspace_entry(name, value))
1887                    .collect();
1888                entries.sort_by(|a, b| a.name.cmp(&b.name));
1889                (entries, true)
1890            }
1891        } else {
1892            (workspace_updates, false)
1893        };
1894        let workspace_snapshot = self.build_workspace_snapshot(workspace_entries, snapshot_full);
1895        let figures_touched = runmat_runtime::plotting_hooks::take_recent_figures();
1896        let stdin_events = stdin_events
1897            .lock()
1898            .map(|guard| guard.clone())
1899            .unwrap_or_default();
1900
1901        let warnings = runmat_runtime::warning_store::take_all();
1902
1903        if let Some(runtime_error) = &mut error {
1904            self.normalize_error_namespace(runtime_error);
1905            self.populate_callstack(runtime_error);
1906        }
1907
1908        let suppress_public_value =
1909            is_expression_stmt && matches!(final_stmt_emit, FinalStmtEmitDisposition::Suppressed);
1910        let public_value = if is_semicolon_suppressed || suppress_public_value {
1911            None
1912        } else {
1913            result_value
1914        };
1915
1916        Ok(ExecutionResult {
1917            value: public_value,
1918            execution_time_ms,
1919            used_jit,
1920            error,
1921            type_info,
1922            streams,
1923            workspace: workspace_snapshot,
1924            figures_touched,
1925            warnings,
1926            profiling: gather_profiling(execution_time_ms),
1927            fusion_plan: fusion_snapshot,
1928            stdin_events,
1929        })
1930    }
1931
1932    /// Get execution statistics
1933    pub fn stats(&self) -> &ExecutionStats {
1934        &self.stats
1935    }
1936
1937    /// Reset execution statistics
1938    pub fn reset_stats(&mut self) {
1939        self.stats = ExecutionStats::default();
1940    }
1941
1942    /// Clear all variables in the session context
1943    pub fn clear_variables(&mut self) {
1944        self.variables.clear();
1945        self.variable_array.clear();
1946        self.variable_names.clear();
1947        self.workspace_values.clear();
1948        self.workspace_preview_tokens.clear();
1949    }
1950
1951    pub async fn export_workspace_state(
1952        &mut self,
1953        mode: WorkspaceExportMode,
1954    ) -> Result<Option<Vec<u8>>> {
1955        if matches!(mode, WorkspaceExportMode::Off) {
1956            return Ok(None);
1957        }
1958
1959        let source_map = if self.workspace_values.is_empty() {
1960            &self.variables
1961        } else {
1962            &self.workspace_values
1963        };
1964
1965        let mut entries: Vec<(String, Value)> = Vec::with_capacity(source_map.len());
1966        for (name, value) in source_map {
1967            let gathered = gather_if_needed_async(value).await?;
1968            entries.push((name.clone(), gathered));
1969        }
1970
1971        if entries.is_empty() && matches!(mode, WorkspaceExportMode::Auto) {
1972            return Ok(None);
1973        }
1974
1975        let replay_mode = match mode {
1976            WorkspaceExportMode::Auto => WorkspaceReplayMode::Auto,
1977            WorkspaceExportMode::Force => WorkspaceReplayMode::Force,
1978            WorkspaceExportMode::Off => WorkspaceReplayMode::Off,
1979        };
1980
1981        runtime_export_workspace_state(&entries, replay_mode)
1982            .await
1983            .map_err(Into::into)
1984    }
1985
1986    pub fn import_workspace_state(&mut self, bytes: &[u8]) -> Result<()> {
1987        let entries = runtime_import_workspace_state(bytes)?;
1988        self.clear_variables();
1989
1990        for (index, (name, value)) in entries.into_iter().enumerate() {
1991            self.variable_names.insert(name.clone(), index);
1992            self.variable_array.push(value.clone());
1993            self.variables.insert(name.clone(), value.clone());
1994            self.workspace_values.insert(name, value);
1995        }
1996
1997        self.workspace_preview_tokens.clear();
1998        self.workspace_version = self.workspace_version.wrapping_add(1);
1999        Ok(())
2000    }
2001
2002    pub fn workspace_snapshot(&mut self) -> WorkspaceSnapshot {
2003        let source_map = if self.workspace_values.is_empty() {
2004            &self.variables
2005        } else {
2006            &self.workspace_values
2007        };
2008
2009        let mut entries: Vec<WorkspaceEntry> = source_map
2010            .iter()
2011            .map(|(name, value)| workspace_entry(name, value))
2012            .collect();
2013        entries.sort_by(|a, b| a.name.cmp(&b.name));
2014
2015        WorkspaceSnapshot {
2016            full: true,
2017            version: self.workspace_version,
2018            values: self.attach_workspace_preview_tokens(entries),
2019        }
2020    }
2021
2022    /// Control whether fusion plan snapshots are emitted in [`ExecutionResult`].
2023    pub fn set_emit_fusion_plan(&mut self, enabled: bool) {
2024        self.emit_fusion_plan = enabled;
2025    }
2026
2027    /// Return the active language compatibility mode.
2028    pub fn compat_mode(&self) -> CompatMode {
2029        self.compat_mode
2030    }
2031
2032    /// Set the language compatibility mode (`matlab` or `strict`).
2033    pub fn set_compat_mode(&mut self, mode: CompatMode) {
2034        self.compat_mode = mode;
2035    }
2036
2037    pub fn set_callstack_limit(&mut self, limit: usize) {
2038        self.callstack_limit = limit;
2039        runmat_ignition::set_call_stack_limit(limit);
2040    }
2041
2042    pub fn set_error_namespace(&mut self, namespace: impl Into<String>) {
2043        let namespace = namespace.into();
2044        let namespace = if namespace.trim().is_empty() {
2045            runmat_ignition::DEFAULT_ERROR_NAMESPACE.to_string()
2046        } else {
2047            namespace
2048        };
2049        self.error_namespace = namespace.clone();
2050        runmat_ignition::set_error_namespace(&namespace);
2051        runmat_hir::set_error_namespace(&namespace);
2052    }
2053
2054    pub fn set_source_name_override(&mut self, name: Option<String>) {
2055        self.source_name_override = name;
2056    }
2057
2058    /// Materialize a workspace variable for inspection (optionally identified by preview token).
2059    pub async fn materialize_variable(
2060        &mut self,
2061        target: WorkspaceMaterializeTarget,
2062        options: WorkspaceMaterializeOptions,
2063    ) -> Result<MaterializedVariable> {
2064        let name = match target {
2065            WorkspaceMaterializeTarget::Name(name) => name,
2066            WorkspaceMaterializeTarget::Token(id) => self
2067                .workspace_preview_tokens
2068                .get(&id)
2069                .map(|ticket| ticket.name.clone())
2070                .ok_or_else(|| anyhow::anyhow!("Unknown workspace preview token"))?,
2071        };
2072        let value = self
2073            .workspace_values
2074            .get(&name)
2075            .or_else(|| self.variables.get(&name))
2076            .cloned()
2077            .ok_or_else(|| anyhow::anyhow!("Variable '{name}' not found in workspace"))?;
2078
2079        let is_gpu = matches!(value, Value::GpuTensor(_));
2080        let residency = if is_gpu {
2081            WorkspaceResidency::Gpu
2082        } else {
2083            WorkspaceResidency::Cpu
2084        };
2085        // For CPU values we can materialize directly. For GPU tensors, avoid downloading the
2086        // entire buffer into wasm memory; gather only the requested preview/slice.
2087        let host_value = value.clone();
2088        let value_shape_vec = value_shape(&host_value).unwrap_or_default();
2089        let mut preview = None;
2090        if is_gpu {
2091            if let Value::GpuTensor(handle) = &value {
2092                if let Some((values, truncated)) =
2093                    gather_gpu_preview_values(handle, &value_shape_vec, &options).await?
2094                {
2095                    preview = Some(WorkspacePreview { values, truncated });
2096                }
2097            }
2098        } else {
2099            if let Some(slice_opts) = options
2100                .slice
2101                .as_ref()
2102                .and_then(|slice| slice.sanitized(&value_shape_vec))
2103            {
2104                let slice_elements = slice_opts.shape.iter().product::<usize>();
2105                let slice_limit = slice_elements.clamp(1, MATERIALIZE_DEFAULT_LIMIT);
2106                if let Some(slice_value) = slice_value_for_preview(&host_value, &slice_opts) {
2107                    preview = preview_numeric_values(&slice_value, slice_limit)
2108                        .map(|(values, truncated)| WorkspacePreview { values, truncated });
2109                }
2110            }
2111            if preview.is_none() {
2112                let max_elements = options.max_elements.clamp(1, MATERIALIZE_DEFAULT_LIMIT);
2113                preview = preview_numeric_values(&host_value, max_elements)
2114                    .map(|(values, truncated)| WorkspacePreview { values, truncated });
2115            }
2116        }
2117        Ok(MaterializedVariable {
2118            name,
2119            class_name: matlab_class_name(&host_value),
2120            dtype: if let Value::GpuTensor(handle) = &host_value {
2121                gpu_dtype_label(handle).map(|label| label.to_string())
2122            } else {
2123                numeric_dtype_label(&host_value).map(|label| label.to_string())
2124            },
2125            shape: value_shape_vec,
2126            is_gpu,
2127            residency,
2128            size_bytes: if let Value::GpuTensor(handle) = &host_value {
2129                gpu_size_bytes(handle)
2130            } else {
2131                approximate_size_bytes(&host_value)
2132            },
2133            preview,
2134            value: host_value,
2135        })
2136    }
2137
2138    /// Get a copy of current variables
2139    pub fn get_variables(&self) -> &HashMap<String, Value> {
2140        &self.variables
2141    }
2142
2143    /// Interpret bytecode with persistent variable context
2144    async fn interpret_with_context(
2145        &mut self,
2146        bytecode: &runmat_ignition::Bytecode,
2147    ) -> Result<runmat_ignition::InterpreterOutcome, RuntimeError> {
2148        let source_name = self.current_source_name().to_string();
2149        runmat_ignition::interpret_with_vars(
2150            bytecode,
2151            &mut self.variable_array,
2152            Some(source_name.as_str()),
2153        )
2154        .await
2155    }
2156
2157    /// Prepare variable array for execution by populating with existing values
2158    fn prepare_variable_array_for_execution(
2159        &mut self,
2160        bytecode: &runmat_ignition::Bytecode,
2161        updated_var_mapping: &HashMap<String, usize>,
2162        debug_trace: bool,
2163    ) {
2164        // Create a new variable array of the correct size
2165        let max_var_id = updated_var_mapping.values().copied().max().unwrap_or(0);
2166        let required_len = std::cmp::max(bytecode.var_count, max_var_id + 1);
2167        let mut new_variable_array = vec![Value::Num(0.0); required_len];
2168        if debug_trace {
2169            debug!(
2170                bytecode_var_count = bytecode.var_count,
2171                required_len, max_var_id, "[repl] prepare variable array"
2172            );
2173        }
2174
2175        // Populate with existing values based on the variable mapping
2176        for (var_name, &new_var_id) in updated_var_mapping {
2177            if new_var_id < new_variable_array.len() {
2178                if let Some(value) = self.workspace_values.get(var_name) {
2179                    if debug_trace {
2180                        debug!(
2181                            var_name,
2182                            var_id = new_var_id,
2183                            ?value,
2184                            "[repl] prepare set var"
2185                        );
2186                    }
2187                    new_variable_array[new_var_id] = value.clone();
2188                }
2189            } else if debug_trace {
2190                debug!(
2191                    var_name,
2192                    var_id = new_var_id,
2193                    len = new_variable_array.len(),
2194                    "[repl] prepare skipping var"
2195                );
2196            }
2197        }
2198
2199        // Update our variable array and mapping
2200        self.variable_array = new_variable_array;
2201    }
2202
2203    /// Convert stored HIR function definitions to UserFunction format for compilation
2204    fn convert_hir_functions_to_user_functions(
2205        &self,
2206    ) -> HashMap<String, runmat_ignition::UserFunction> {
2207        let mut user_functions = HashMap::new();
2208
2209        for (name, hir_stmt) in &self.function_definitions {
2210            if let runmat_hir::HirStmt::Function {
2211                name: func_name,
2212                params,
2213                outputs,
2214                body,
2215                has_varargin: _,
2216                has_varargout: _,
2217                ..
2218            } = hir_stmt
2219            {
2220                // Use the existing HIR utilities to calculate variable count
2221                let var_map =
2222                    runmat_hir::remapping::create_complete_function_var_map(params, outputs, body);
2223                let max_local_var = var_map.len();
2224
2225                let source_id = self.function_source_ids.get(name).copied();
2226                if let Some(id) = source_id {
2227                    if let Some(source) = self.source_pool.get(id) {
2228                        let _ = (&source.name, &source.text);
2229                    }
2230                }
2231                let user_func = runmat_ignition::UserFunction {
2232                    name: func_name.clone(),
2233                    params: params.clone(),
2234                    outputs: outputs.clone(),
2235                    body: body.clone(),
2236                    local_var_count: max_local_var,
2237                    has_varargin: false,
2238                    has_varargout: false,
2239                    var_types: vec![Type::Unknown; max_local_var],
2240                    source_id,
2241                };
2242                user_functions.insert(name.clone(), user_func);
2243            }
2244        }
2245
2246        user_functions
2247    }
2248
2249    /// Configure garbage collector
2250    pub fn configure_gc(&self, config: GcConfig) -> Result<()> {
2251        gc_configure(config)
2252            .map_err(|e| anyhow::anyhow!("Failed to configure garbage collector: {}", e))
2253    }
2254
2255    /// Get GC statistics
2256    pub fn gc_stats(&self) -> runmat_gc::GcStats {
2257        gc_stats()
2258    }
2259
2260    /// Show detailed system information
2261    pub fn show_system_info(&self) {
2262        let gc_stats = self.gc_stats();
2263        info!(
2264            jit = %if self.has_jit() { "available" } else { "disabled/failed" },
2265            verbose = self.verbose,
2266            total_executions = self.stats.total_executions,
2267            jit_compiled = self.stats.jit_compiled,
2268            interpreter_fallback = self.stats.interpreter_fallback,
2269            avg_time_ms = self.stats.average_execution_time_ms,
2270            total_allocations = gc_stats
2271                .total_allocations
2272                .load(std::sync::atomic::Ordering::Relaxed),
2273            minor_collections = gc_stats
2274                .minor_collections
2275                .load(std::sync::atomic::Ordering::Relaxed),
2276            major_collections = gc_stats
2277                .major_collections
2278                .load(std::sync::atomic::Ordering::Relaxed),
2279            current_memory_mb = gc_stats
2280                .current_memory_usage
2281                .load(std::sync::atomic::Ordering::Relaxed) as f64
2282                / 1024.0
2283                / 1024.0,
2284            workspace_vars = self.workspace_values.len(),
2285            "RunMat Session Status"
2286        );
2287    }
2288
2289    #[cfg(feature = "jit")]
2290    fn has_jit(&self) -> bool {
2291        self.jit_engine.is_some()
2292    }
2293
2294    #[cfg(not(feature = "jit"))]
2295    fn has_jit(&self) -> bool {
2296        false
2297    }
2298
2299    fn build_workspace_snapshot(
2300        &mut self,
2301        entries: Vec<WorkspaceEntry>,
2302        full: bool,
2303    ) -> WorkspaceSnapshot {
2304        self.workspace_version = self.workspace_version.wrapping_add(1);
2305        let version = self.workspace_version;
2306        WorkspaceSnapshot {
2307            full,
2308            version,
2309            values: self.attach_workspace_preview_tokens(entries),
2310        }
2311    }
2312
2313    fn attach_workspace_preview_tokens(
2314        &mut self,
2315        entries: Vec<WorkspaceEntry>,
2316    ) -> Vec<WorkspaceEntry> {
2317        self.workspace_preview_tokens.clear();
2318        let mut values = Vec::with_capacity(entries.len());
2319        for mut entry in entries {
2320            let token = Uuid::new_v4();
2321            self.workspace_preview_tokens.insert(
2322                token,
2323                WorkspaceMaterializeTicket {
2324                    name: entry.name.clone(),
2325                },
2326            );
2327            entry.preview_token = Some(token);
2328            values.push(entry);
2329        }
2330        values
2331    }
2332}
2333
2334fn last_displayable_statement_emit_disposition(
2335    body: &[runmat_hir::HirStmt],
2336) -> FinalStmtEmitDisposition {
2337    use runmat_hir::HirStmt;
2338
2339    for stmt in body.iter().rev() {
2340        match stmt {
2341            HirStmt::ExprStmt(expr, _, _) => return expr_emit_disposition(expr),
2342            HirStmt::Assign(_, _, _, _) | HirStmt::MultiAssign(_, _, _, _) => {
2343                return FinalStmtEmitDisposition::Suppressed
2344            }
2345            HirStmt::AssignLValue(_, _, _, _) => return FinalStmtEmitDisposition::Suppressed,
2346
2347            _ => continue,
2348        }
2349    }
2350    FinalStmtEmitDisposition::Suppressed
2351}
2352
2353fn last_unsuppressed_assign_var(body: &[runmat_hir::HirStmt]) -> Option<usize> {
2354    use runmat_hir::HirStmt;
2355
2356    for stmt in body.iter().rev() {
2357        match stmt {
2358            HirStmt::Assign(var_id, _, suppressed, _) => {
2359                return if *suppressed { None } else { Some(var_id.0) };
2360            }
2361            HirStmt::ExprStmt(_, _, _)
2362            | HirStmt::MultiAssign(_, _, _, _)
2363            | HirStmt::AssignLValue(_, _, _, _) => return None,
2364            _ => continue,
2365        }
2366    }
2367    None
2368}
2369
2370fn last_expr_emits_value(body: &[runmat_hir::HirStmt]) -> bool {
2371    use runmat_hir::HirStmt;
2372
2373    for stmt in body.iter().rev() {
2374        match stmt {
2375            HirStmt::ExprStmt(expr, suppressed, _) => {
2376                if *suppressed {
2377                    return false;
2378                }
2379                return matches!(
2380                    expr_emit_disposition(expr),
2381                    FinalStmtEmitDisposition::Inline
2382                );
2383            }
2384            HirStmt::Assign(_, _, _, _)
2385            | HirStmt::MultiAssign(_, _, _, _)
2386            | HirStmt::AssignLValue(_, _, _, _) => return false,
2387            _ => continue,
2388        }
2389    }
2390    false
2391}
2392
2393fn last_emit_var_index(bytecode: &runmat_ignition::Bytecode) -> Option<usize> {
2394    for instr in bytecode.instructions.iter().rev() {
2395        if let runmat_ignition::Instr::EmitVar { var_index, .. } = instr {
2396            return Some(*var_index);
2397        }
2398    }
2399    None
2400}
2401
2402fn expr_emit_disposition(expr: &runmat_hir::HirExpr) -> FinalStmtEmitDisposition {
2403    use runmat_hir::HirExprKind;
2404    if let HirExprKind::FuncCall(name, _) = &expr.kind {
2405        if runmat_builtins::suppresses_auto_output(name) {
2406            return FinalStmtEmitDisposition::Suppressed;
2407        }
2408    }
2409    FinalStmtEmitDisposition::Inline
2410}
2411
2412const WORKSPACE_PREVIEW_LIMIT: usize = 16;
2413const MATERIALIZE_DEFAULT_LIMIT: usize = 4096;
2414
2415fn workspace_entry(name: &str, value: &Value) -> WorkspaceEntry {
2416    let dtype = numeric_dtype_label(value).map(|label| label.to_string());
2417    let preview = preview_numeric_values(value, WORKSPACE_PREVIEW_LIMIT)
2418        .map(|(values, truncated)| WorkspacePreview { values, truncated });
2419    let residency = if matches!(value, Value::GpuTensor(_)) {
2420        WorkspaceResidency::Gpu
2421    } else {
2422        WorkspaceResidency::Cpu
2423    };
2424    WorkspaceEntry {
2425        name: name.to_string(),
2426        class_name: matlab_class_name(value),
2427        dtype,
2428        shape: value_shape(value).unwrap_or_default(),
2429        is_gpu: matches!(value, Value::GpuTensor(_)),
2430        size_bytes: approximate_size_bytes(value),
2431        preview,
2432        residency,
2433        preview_token: None,
2434    }
2435}
2436
2437struct ActiveExecutionGuard {
2438    flag: *mut bool,
2439}
2440
2441impl ActiveExecutionGuard {
2442    fn new(session: &mut RunMatSession) -> Result<Self> {
2443        if session.is_executing {
2444            Err(anyhow::anyhow!(
2445                "RunMatSession is already executing another script"
2446            ))
2447        } else {
2448            session.is_executing = true;
2449            Ok(Self {
2450                flag: &mut session.is_executing,
2451            })
2452        }
2453    }
2454}
2455
2456impl Drop for ActiveExecutionGuard {
2457    fn drop(&mut self) {
2458        unsafe {
2459            if let Some(flag) = self.flag.as_mut() {
2460                *flag = false;
2461            }
2462        }
2463    }
2464}
2465
2466impl Default for RunMatSession {
2467    fn default() -> Self {
2468        Self::new().expect("Failed to create default RunMat session")
2469    }
2470}
2471
2472/// Tokenize the input string and return a space separated string of token names.
2473/// This is kept for backward compatibility with existing tests.
2474pub fn format_tokens(input: &str) -> String {
2475    tokenize_detailed(input)
2476        .into_iter()
2477        .map(|t| format!("{:?}", t.token))
2478        .collect::<Vec<_>>()
2479        .join(" ")
2480}
2481
2482/// Execute MATLAB/Octave code and return the result as a formatted string
2483pub async fn execute_and_format(input: &str) -> String {
2484    match RunMatSession::new() {
2485        Ok(mut engine) => match engine.execute(input).await {
2486            Ok(result) => {
2487                if let Some(error) = result.error {
2488                    format!("Error: {error}")
2489                } else if let Some(value) = result.value {
2490                    format!("{value:?}")
2491                } else {
2492                    "".to_string()
2493                }
2494            }
2495            Err(e) => format!("Error: {e}"),
2496        },
2497        Err(e) => format!("Engine Error: {e}"),
2498    }
2499}
2500
2501#[cfg(not(target_arch = "wasm32"))]
2502fn reset_provider_telemetry() {
2503    if let Some(provider) = accel_provider() {
2504        provider.reset_telemetry();
2505    }
2506}
2507
2508#[cfg(target_arch = "wasm32")]
2509fn reset_provider_telemetry() {}
2510
2511#[cfg(not(target_arch = "wasm32"))]
2512fn gather_profiling(execution_time_ms: u64) -> Option<ExecutionProfiling> {
2513    let provider = accel_provider()?;
2514    let telemetry = provider.telemetry_snapshot();
2515    let gpu_ns = telemetry.fused_elementwise.total_wall_time_ns
2516        + telemetry.fused_reduction.total_wall_time_ns
2517        + telemetry.matmul.total_wall_time_ns;
2518    let gpu_ms = gpu_ns.saturating_div(1_000_000);
2519    Some(ExecutionProfiling {
2520        total_ms: execution_time_ms,
2521        cpu_ms: Some(execution_time_ms.saturating_sub(gpu_ms)),
2522        gpu_ms: Some(gpu_ms),
2523        kernel_count: Some(
2524            (telemetry.fused_elementwise.count
2525                + telemetry.fused_reduction.count
2526                + telemetry.matmul.count
2527                + telemetry.kernel_launches.len() as u64)
2528                .min(u32::MAX as u64) as u32,
2529        ),
2530    })
2531}
2532
2533#[cfg(target_arch = "wasm32")]
2534fn gather_profiling(execution_time_ms: u64) -> Option<ExecutionProfiling> {
2535    Some(ExecutionProfiling {
2536        total_ms: execution_time_ms,
2537        ..ExecutionProfiling::default()
2538    })
2539}
2540
2541#[cfg(test)]
2542mod tests {
2543    use super::*;
2544    use futures::executor::block_on;
2545
2546    #[test]
2547    fn captures_basic_workspace_assignments() {
2548        let mut session =
2549            RunMatSession::with_snapshot_bytes(false, false, None).expect("session init");
2550        let result = block_on(session.execute("x = 42;")).expect("exec succeeds");
2551        assert!(
2552            result
2553                .workspace
2554                .values
2555                .iter()
2556                .any(|entry| entry.name == "x"),
2557            "workspace snapshot should include assigned variable"
2558        );
2559    }
2560
2561    #[test]
2562    fn workspace_reports_datetime_array_shape() {
2563        let mut session =
2564            RunMatSession::with_snapshot_bytes(false, false, None).expect("session init");
2565        let result =
2566            block_on(session.execute("d = datetime([739351; 739352], 'ConvertFrom', 'datenum');"))
2567                .expect("exec succeeds");
2568        let entry = result
2569            .workspace
2570            .values
2571            .iter()
2572            .find(|entry| entry.name == "d")
2573            .expect("workspace entry for d");
2574        assert_eq!(entry.class_name, "datetime");
2575        assert_eq!(entry.shape, vec![2, 1]);
2576    }
2577
2578    #[test]
2579    fn workspace_state_roundtrip_replace_only() {
2580        let mut source_session =
2581            RunMatSession::with_snapshot_bytes(false, false, None).expect("session init");
2582        let _ = block_on(source_session.execute("x = 42; y = [1, 2, 3];")).expect("exec succeeds");
2583
2584        let bytes = block_on(source_session.export_workspace_state(WorkspaceExportMode::Force))
2585            .expect("workspace export")
2586            .expect("workspace bytes");
2587
2588        let mut restore_session =
2589            RunMatSession::with_snapshot_bytes(false, false, None).expect("session init");
2590        let _ = block_on(restore_session.execute("z = 99;")).expect("seed workspace");
2591        restore_session
2592            .import_workspace_state(&bytes)
2593            .expect("workspace import");
2594
2595        let _restored =
2596            block_on(restore_session.execute("r = x + y(2);")).expect("post-import exec");
2597
2598        let x = block_on(restore_session.materialize_variable(
2599            WorkspaceMaterializeTarget::Name("x".to_string()),
2600            WorkspaceMaterializeOptions::default(),
2601        ))
2602        .expect("x should exist after import");
2603        assert_eq!(x.name, "x");
2604
2605        let y = block_on(restore_session.materialize_variable(
2606            WorkspaceMaterializeTarget::Name("y".to_string()),
2607            WorkspaceMaterializeOptions::default(),
2608        ))
2609        .expect("y should exist after import");
2610        assert_eq!(y.name, "y");
2611
2612        let z = block_on(restore_session.materialize_variable(
2613            WorkspaceMaterializeTarget::Name("z".to_string()),
2614            WorkspaceMaterializeOptions::default(),
2615        ));
2616        assert!(
2617            z.is_err(),
2618            "replace-only import should drop stale z variable"
2619        );
2620    }
2621
2622    #[test]
2623    fn workspace_state_import_rejects_invalid_payload() {
2624        let mut session =
2625            RunMatSession::with_snapshot_bytes(false, false, None).expect("session init");
2626        let err = session
2627            .import_workspace_state(&[1, 2, 3, 4])
2628            .expect_err("invalid payload should be rejected");
2629        let runtime_err = err
2630            .downcast_ref::<runmat_runtime::RuntimeError>()
2631            .expect("error should preserve runtime replay details");
2632        assert_eq!(
2633            runtime_err.identifier(),
2634            Some("RunMat:ReplayDecodeFailed"),
2635            "invalid payload should map to replay decode identifier"
2636        );
2637    }
2638}