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