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
56pub struct RunMatSession {
58 #[cfg(feature = "jit")]
60 jit_engine: Option<TurbineEngine>,
61 verbose: bool,
63 stats: ExecutionStats,
65 variables: HashMap<String, Value>,
67 variable_array: Vec<Value>,
69 variable_names: HashMap<String, usize>,
71 workspace_values: HashMap<String, Value>,
73 function_definitions: HashMap<String, runmat_hir::HirStmt>,
75 source_pool: SourcePool,
77 function_source_ids: HashMap<String, SourceId>,
79 snapshot: Option<Arc<Snapshot>>,
81 interrupt_flag: Arc<AtomicBool>,
83 is_executing: bool,
85 async_input_handler: Option<SharedAsyncInputHandler>,
88 callstack_limit: usize,
90 error_namespace: String,
92 default_source_name: String,
94 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 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 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 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 pub type_info: Option<String>,
699 pub streams: Vec<ExecutionStreamEntry>,
701 pub workspace: WorkspaceSnapshot,
703 pub figures_touched: Vec<u32>,
705 pub warnings: Vec<RuntimeWarning>,
707 pub profiling: Option<ExecutionProfiling>,
709 pub fusion_plan: Option<FusionPlanSnapshot>,
711 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
743fn 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 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 pub fn new() -> Result<Self> {
806 Self::with_options(true, false) }
808
809 pub fn with_options(enable_jit: bool, verbose: bool) -> Result<Self> {
811 Self::from_snapshot(enable_jit, verbose, None)
812 }
813
814 #[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 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 #[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 #[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 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 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 pub fn cancel_execution(&self) {
1032 self.interrupt_flag.store(true, Ordering::Relaxed);
1033 }
1034
1035 pub fn interrupt_handle(&self) -> Arc<AtomicBool> {
1037 Arc::clone(&self.interrupt_flag)
1038 }
1039
1040 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 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 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 pub async fn execute(&mut self, input: &str) -> std::result::Result<ExecutionResult, RunError> {
1139 self.run(input).await
1140 }
1141
1142 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 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 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 Box::pin(eval_expr(expr, compat))
1312 }
1313
1314 #[cfg(not(target_arch = "wasm32"))]
1315 {
1316 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 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; let mut suppressed_value: Option<Value> = None; 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 let is_expression_stmt = bytecode
1450 .instructions
1451 .last()
1452 .map(|instr| matches!(instr, runmat_ignition::Instr::Pop))
1453 .unwrap_or(false);
1454
1455 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 #[cfg(feature = "jit")]
1485 {
1486 if let Some(ref mut jit_engine) = &mut self.jit_engine {
1487 if !is_expression_stmt {
1488 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 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 }
1552 }
1553 }
1554 }
1555 }
1556
1557 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 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(); 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; 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 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 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 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 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; 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 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 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 ¤t_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 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 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 pub fn stats(&self) -> &ExecutionStats {
1934 &self.stats
1935 }
1936
1937 pub fn reset_stats(&mut self) {
1939 self.stats = ExecutionStats::default();
1940 }
1941
1942 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 pub fn set_emit_fusion_plan(&mut self, enabled: bool) {
2024 self.emit_fusion_plan = enabled;
2025 }
2026
2027 pub fn compat_mode(&self) -> CompatMode {
2029 self.compat_mode
2030 }
2031
2032 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 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 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 pub fn get_variables(&self) -> &HashMap<String, Value> {
2140 &self.variables
2141 }
2142
2143 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 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 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 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 self.variable_array = new_variable_array;
2201 }
2202
2203 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 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 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 pub fn gc_stats(&self) -> runmat_gc::GcStats {
2257 gc_stats()
2258 }
2259
2260 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
2472pub 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
2482pub 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}