Skip to main content

runmat_core/session/
run.rs

1use super::*;
2
3#[cfg(not(target_arch = "wasm32"))]
4fn entrypoint_target_function(
5    assembly: &runmat_hir::HirAssembly,
6) -> Option<runmat_hir::FunctionId> {
7    assembly
8        .entrypoints
9        .first()
10        .map(|entrypoint| entrypoint.target)
11}
12
13#[cfg(not(target_arch = "wasm32"))]
14fn mir_local_fact_count_for_entrypoint(
15    analysis: &runmat_mir::analysis::AnalysisStore,
16    assembly: &runmat_hir::HirAssembly,
17) -> usize {
18    let Some(entrypoint_target) = entrypoint_target_function(assembly) else {
19        return analysis.mir_locals.len();
20    };
21    analysis
22        .mir_locals
23        .keys()
24        .filter(|key| key.function == entrypoint_target)
25        .count()
26}
27
28fn discover_known_project_symbols(source_name: Option<&str>) -> HashSet<String> {
29    use runmat_config::project::discover_known_project_symbols_from_source_name;
30    use std::path::{Path, PathBuf};
31
32    let Ok(cwd) = runmat_filesystem::current_dir() else {
33        let Some(source_name) = source_name else {
34            return HashSet::new();
35        };
36        let source_path = PathBuf::from(source_name);
37        if source_path.is_absolute() {
38            let source_cwd = source_path
39                .parent()
40                .map(Path::to_path_buf)
41                .unwrap_or_else(|| PathBuf::from("/"));
42            return discover_known_project_symbols_from_source_name(Some(source_name), &source_cwd);
43        }
44        return HashSet::new();
45    };
46    discover_known_project_symbols_from_source_name(source_name, &cwd)
47}
48
49impl RunMatSession {
50    async fn run(
51        &mut self,
52        input: &str,
53    ) -> std::result::Result<crate::abi::ExecutionOutcome, RunError> {
54        let companion = super::compile::discover_companion_source_statements_async(
55            self.current_source_name(),
56            self.compat_mode,
57        )
58        .await;
59        self.pending_companion_source_discovery = Some(companion);
60        let previous_workspace_names = self
61            .workspace_values
62            .keys()
63            .cloned()
64            .collect::<HashSet<_>>();
65        let mut execution = self.execute_internal(input, true).await?;
66        let workspace_names = execution
67            .workspace_snapshot
68            .values
69            .iter()
70            .map(|entry| entry.name.clone())
71            .collect::<Vec<_>>();
72        let workspace_full = execution.workspace_snapshot.full;
73        let outcome = &mut execution.outcome;
74        outcome.workspace_delta.upserts = self.abi_workspace_upserts(workspace_names);
75        if workspace_full {
76            outcome.workspace_delta.removals =
77                self.abi_workspace_removals(previous_workspace_names);
78            if !outcome.workspace_delta.removals.is_empty() {
79                outcome.effects.push(crate::abi::ObservedEffect::Workspace(
80                    crate::abi::WorkspaceEffectKind::Clear,
81                ));
82            }
83        }
84        Ok(execution.outcome)
85    }
86
87    /// Execute a structured runtime/workspace ABI request.
88    pub async fn execute_request(
89        &mut self,
90        request: crate::abi::ExecutionRequest,
91    ) -> crate::abi::ExecutionResponse {
92        let requested_outputs = request.requested_outputs.clone();
93        let source_input = request.source.clone();
94        let (source_name, source_text) = match source_input_text(request.source).await {
95            Ok(resolved) => resolved,
96            Err(err) => {
97                return crate::abi::ExecutionResponse {
98                    source_context: unresolved_source_context(&source_input),
99                    result: Err(err),
100                };
101            }
102        };
103        let source_identity = resolve_source_identity(&source_input, &source_text);
104        let previous_compat = self.compat_mode;
105        let previous_top_level_await_enabled = self.top_level_await_enabled;
106        let previous_dynamic_eval_enabled = self.dynamic_eval_enabled;
107        let previous_source_name = self.active_source_name.clone();
108        let previous_workspace_handle = self.abi_workspace_handle;
109        let previous_source_identity = self.active_source_identity.clone();
110
111        self.compat_mode = request.compatibility;
112        self.top_level_await_enabled = request.host_policy.top_level_await;
113        self.dynamic_eval_enabled = request.host_policy.dynamic_eval;
114        self.active_source_name = source_name.clone();
115        self.abi_workspace_handle = request.workspace;
116        self.active_source_identity = source_identity.clone();
117
118        let result = self.run(&source_text).await;
119
120        self.compat_mode = previous_compat;
121        self.top_level_await_enabled = previous_top_level_await_enabled;
122        self.dynamic_eval_enabled = previous_dynamic_eval_enabled;
123        self.active_source_name = previous_source_name;
124        self.abi_workspace_handle = previous_workspace_handle;
125        self.active_source_identity = previous_source_identity;
126        self.pending_companion_source_discovery = None;
127
128        crate::abi::ExecutionResponse {
129            source_context: crate::abi::ExecutionSourceContext {
130                name: source_name,
131                text: Some(source_text),
132                identity: source_identity,
133            },
134            result: result
135                .map(|outcome| apply_requested_output_policy(outcome, &requested_outputs)),
136        }
137    }
138
139    async fn execute_internal(
140        &mut self,
141        input: &str,
142        preserve_layout_var_names: bool,
143    ) -> std::result::Result<SessionExecution, RunError> {
144        let _active = ActiveExecutionGuard::new(self).map_err(|err| {
145            RunError::Runtime(
146                build_runtime_error(err.to_string())
147                    .with_identifier("RunMat:ExecutionAlreadyActive")
148                    .build(),
149            )
150        })?;
151        runmat_vm::set_call_stack_limit(self.callstack_limit);
152        runmat_vm::set_error_namespace(&self.error_namespace);
153        runmat_vm::set_dynamic_eval_options(
154            self.compat_mode,
155            self.compat_mode.allows_runmat_extensions(),
156            self.top_level_await_enabled,
157            self.dynamic_eval_enabled,
158        );
159        runmat_hir::set_error_namespace(&self.error_namespace);
160        let exec_span = info_span!(
161            "runtime.execute",
162            input_len = input.len(),
163            verbose = self.verbose
164        );
165        let _exec_guard = exec_span.enter();
166        runmat_runtime::console::reset_thread_buffer();
167        runmat_runtime::plotting_hooks::reset_recent_figures();
168        runmat_runtime::warning_store::reset();
169        runmat_builtins::set_display_format(self.format_mode);
170        reset_provider_telemetry();
171        self.interrupt_flag.store(false, Ordering::Relaxed);
172        let _interrupt_guard =
173            runmat_runtime::interrupt::replace_interrupt(Some(self.interrupt_flag.clone()));
174        let start_time = Instant::now();
175        self.stats.total_executions += 1;
176        let debug_trace = std::env::var("RUNMAT_DEBUG_REPL").is_ok();
177        let stdin_events: Arc<Mutex<Vec<StdinEvent>>> = Arc::new(Mutex::new(Vec::new()));
178        let host_async_handler = self.async_input_handler.clone();
179        let stdin_events_async = Arc::clone(&stdin_events);
180        let runtime_async_handler: Arc<runmat_runtime::interaction::AsyncInteractionHandler> =
181            Arc::new(
182                move |prompt: runmat_runtime::interaction::InteractionPromptOwned| {
183                    let request_kind = match prompt.kind {
184                        runmat_runtime::interaction::InteractionKind::Line { echo } => {
185                            InputRequestKind::Line { echo }
186                        }
187                        runmat_runtime::interaction::InteractionKind::KeyPress => {
188                            InputRequestKind::KeyPress
189                        }
190                    };
191                    let request = InputRequest {
192                        prompt: prompt.prompt,
193                        kind: request_kind,
194                    };
195                    let (event_kind, echo_flag) = match &request.kind {
196                        InputRequestKind::Line { echo } => (StdinEventKind::Line, *echo),
197                        InputRequestKind::KeyPress => (StdinEventKind::KeyPress, false),
198                    };
199                    let mut event = StdinEvent {
200                        prompt: request.prompt.clone(),
201                        kind: event_kind,
202                        echo: echo_flag,
203                        value: None,
204                        error: None,
205                    };
206
207                    let stdin_events_async = Arc::clone(&stdin_events_async);
208                    let host_async_handler = host_async_handler.clone();
209                    Box::pin(async move {
210                        let resp: Result<InputResponse, String> =
211                            if let Some(handler) = host_async_handler {
212                                handler(request).await
213                            } else {
214                                match &request.kind {
215                                    InputRequestKind::Line { echo } => {
216                                        runmat_runtime::interaction::default_read_line(
217                                            &request.prompt,
218                                            *echo,
219                                        )
220                                        .map(InputResponse::Line)
221                                    }
222                                    InputRequestKind::KeyPress => {
223                                        runmat_runtime::interaction::default_wait_for_key(
224                                            &request.prompt,
225                                        )
226                                        .map(|_| InputResponse::KeyPress)
227                                    }
228                                }
229                            };
230
231                        let resp = resp.inspect_err(|err| {
232                            event.error = Some(err.clone());
233                            if let Ok(mut guard) = stdin_events_async.lock() {
234                                guard.push(event.clone());
235                            }
236                        })?;
237
238                        let interaction_resp = match resp {
239                            InputResponse::Line(value) => {
240                                event.value = Some(value.clone());
241                                if let Ok(mut guard) = stdin_events_async.lock() {
242                                    guard.push(event);
243                                }
244                                runmat_runtime::interaction::InteractionResponse::Line(value)
245                            }
246                            InputResponse::KeyPress => {
247                                if let Ok(mut guard) = stdin_events_async.lock() {
248                                    guard.push(event);
249                                }
250                                runmat_runtime::interaction::InteractionResponse::KeyPress
251                            }
252                        };
253                        Ok(interaction_resp)
254                    })
255                },
256            );
257        let _async_input_guard =
258            runmat_runtime::interaction::replace_async_handler(Some(runtime_async_handler));
259
260        // Install a stateless expression evaluator for `input()` numeric parsing.
261        //
262        // The hook runs the full parse → lower → compile → interpret pipeline so
263        // that users can type arbitrary MATLAB expressions at an input() prompt:
264        // `sqrt(2)`, `pi/2`, `ones(3)`, `[1 2; 3 4]`, etc.
265        //
266        // Stack-overflow hazard: the hook calls runmat_vm::interpret() while
267        // the outer interpret() is already on the call stack. On WASM the JS event
268        // loop drives both as async state-machines and the WASM linear stack is
269        // large, so nesting is safe. On native the default thread stack is too
270        // small for two nested interpret() invocations, so we instead run the inner
271        // interpret() on a dedicated thread that has its own 16 MB stack and block
272        // the calling future synchronously on the result (safe because the native
273        // executor — futures::executor::block_on — is already synchronous).
274        let compat = self.compat_mode;
275        let top_level_await_enabled = self.top_level_await_enabled;
276        let source_name_for_eval_hook = self.current_source_name().to_string();
277        let known_project_symbols_for_eval_hook = Arc::new(discover_known_project_symbols(Some(
278            source_name_for_eval_hook.as_str(),
279        )));
280        let _eval_hook_guard =
281            runmat_runtime::interaction::replace_eval_hook(Some(std::sync::Arc::new(
282                move |expr: String| -> runmat_runtime::interaction::EvalHookFuture {
283                    // Shared eval logic, used by both the WASM async path and the
284                    // native thread path below.
285                    async fn eval_expr(
286                        expr: String,
287                        compat: runmat_parser::CompatMode,
288                        top_level_await_enabled: bool,
289                        known_project_symbols: Arc<HashSet<String>>,
290                    ) -> Result<Value, RuntimeError> {
291                        let wrapped = format!("__runmat_input_result__ = ({expr});");
292                        let ast = parse_with_options(&wrapped, ParserOptions::new(compat))
293                            .map_err(|e| {
294                                build_runtime_error(format!("input: parse error: {e}"))
295                                    .with_identifier("RunMat:input:ParseError")
296                                    .build()
297                            })?;
298                        let lowering = runmat_hir::lower(
299                            &ast,
300                            &LoweringContext::new(&HashMap::new())
301                                .with_known_project_symbols(&known_project_symbols)
302                                .with_runmat_extensions_enabled(compat.allows_runmat_extensions())
303                                .with_top_level_await_enabled(top_level_await_enabled),
304                        )
305                        .map_err(|e| {
306                            build_runtime_error(format!("input: lowering error: {e}"))
307                                .with_identifier("RunMat:input:LowerError")
308                                .build()
309                        })?;
310                        let bc =
311                            compile_eval_hook_bytecode(&lowering).map_err(RuntimeError::from)?;
312                        let result_idx = bc.var_names.iter().find_map(|(idx, name)| {
313                            (name == "__runmat_input_result__").then_some(*idx)
314                        });
315                        let vars = runmat_vm::interpret(&bc).await?;
316                        result_idx
317                            .and_then(|idx| vars.get(idx).cloned())
318                            .ok_or_else(|| {
319                                build_runtime_error("input: expression produced no value")
320                                    .with_identifier("RunMat:input:NoValue")
321                                    .build()
322                            })
323                    }
324
325                    #[cfg(target_arch = "wasm32")]
326                    {
327                        // On WASM: await the inner interpret() directly. The JS async
328                        // runtime handles both futures as cooperative state-machines and
329                        // the WASM linear stack is large enough for the extra frames.
330                        Box::pin(eval_expr(
331                            expr,
332                            compat,
333                            top_level_await_enabled,
334                            Arc::clone(&known_project_symbols_for_eval_hook),
335                        ))
336                    }
337
338                    #[cfg(not(target_arch = "wasm32"))]
339                    {
340                        // On native: run interpret() on a dedicated thread so it gets
341                        // its own 16 MB stack, fully isolated from the outer interpret()
342                        // call stack. The result is sent back via a tokio oneshot channel
343                        // and awaited asynchronously so the tokio worker thread is never
344                        // blocked by a synchronous recv().
345                        let (tx, rx) = tokio::sync::oneshot::channel();
346                        let known_project_symbols =
347                            Arc::clone(&known_project_symbols_for_eval_hook);
348                        let spawn_result = std::thread::Builder::new()
349                            .stack_size(16 * 1024 * 1024)
350                            .spawn(move || {
351                                let result = futures::executor::block_on(eval_expr(
352                                    expr,
353                                    compat,
354                                    top_level_await_enabled,
355                                    known_project_symbols,
356                                ));
357                                let _ = tx.send(result);
358                            });
359                        Box::pin(async move {
360                            spawn_result.map_err(|err| {
361                                build_runtime_error(format!(
362                                    "input: failed to spawn eval thread: {err}"
363                                ))
364                                .with_identifier("RunMat:input:EvalThreadSpawnFailed")
365                                .build()
366                            })?;
367                            rx.await.unwrap_or_else(|_| {
368                                Err(build_runtime_error("input: eval thread panicked")
369                                    .with_identifier("RunMat:input:EvalThreadPanic")
370                                    .build())
371                            })
372                        })
373                    }
374                },
375            )));
376
377        if self.verbose {
378            debug!("Executing: {}", input.trim());
379        }
380
381        let source_name_for_context = self.current_source_name().to_string();
382        let _fallback_source_guard = runmat_runtime::source_context::replace_current_source_context(
383            Some(&source_name_for_context),
384            Some(input),
385        );
386
387        let PreparedExecution {
388            ast,
389            lowering,
390            analysis,
391            mut bytecode,
392            function_registry_after_success,
393            next_semantic_function_id_after_success,
394        } = self.compile_input(input)?;
395        let source_catalog_entries = self
396            .source_pool
397            .entries()
398            .map(|(source_id, source)| {
399                (source_id, source.name.to_string(), source.text.to_string())
400            })
401            .collect::<Vec<_>>();
402        let _source_catalog_guard =
403            runmat_runtime::source_context::replace_source_catalog(source_catalog_entries);
404        let _source_id_guard =
405            runmat_runtime::source_context::replace_current_source_id(bytecode.source_id);
406        #[cfg(target_arch = "wasm32")]
407        let _ = &analysis;
408        if self.verbose {
409            debug!("AST: {ast:?}");
410        }
411        let display = execution_display_context(&lowering.assembly, bytecode.layout.as_ref());
412        let display_context = display.context;
413        let display_var_ids = display.display_var_ids;
414        let stmt_count = entry_statement_count(&lowering.assembly);
415        let execution_vars = execution_workspace_mapping(&bytecode);
416        let max_var_id = execution_vars.values().copied().max().unwrap_or(0);
417        if debug_trace {
418            debug!(?execution_vars, "[repl] execution vars");
419        }
420        if debug_trace {
421            debug!(workspace_values_before = ?self.workspace_values, "[repl] workspace snapshot before execution");
422        }
423        let id_to_name: HashMap<usize, String> = execution_vars
424            .iter()
425            .map(|(name, var_id)| (*var_id, name.clone()))
426            .collect();
427        let mut assigned_this_execution: HashSet<String> = HashSet::new();
428        let assigned_snapshot: HashSet<String> = execution_vars
429            .keys()
430            .filter(|name| self.workspace_values.contains_key(name.as_str()))
431            .cloned()
432            .collect();
433        let prev_assigned_snapshot = assigned_snapshot.clone();
434        if debug_trace {
435            debug!(?assigned_snapshot, "[repl] assigned snapshot");
436        }
437        let _pending_workspace_guard =
438            runmat_vm::push_pending_workspace(execution_vars.clone(), assigned_snapshot.clone());
439        if self.verbose {
440            debug!("HIR generated successfully");
441        }
442
443        if preserve_layout_var_names && bytecode.layout.is_some() {
444            for (slot, name) in &id_to_name {
445                bytecode.var_names.insert(*slot, name.clone());
446            }
447        } else {
448            bytecode.var_names = id_to_name.clone();
449        }
450        if self.verbose {
451            debug!(
452                "Bytecode compiled: {} instructions",
453                bytecode.instructions.len()
454            );
455        }
456
457        #[cfg(not(target_arch = "wasm32"))]
458        let fusion_snapshot = if self.emit_fusion_plan {
459            let runtime_groups = bytecode.runtime_fusion_groups();
460            let (runtime_graph, runtime_graph_source) =
461                bytecode.runtime_accel_graph_for_fusion_with_source(&runtime_groups);
462            build_fusion_snapshot(
463                &runtime_groups,
464                &bytecode.fusion_metadata.mir_fusion_candidate_groups,
465                &bytecode.fusion_metadata.instruction_windows,
466                Some(crate::fusion::FusionPlannerMetadata {
467                    source: "semantic-mir-analysis-runtime".to_string(),
468                    accel_graph_state: if runtime_graph.is_some() {
469                        "present".to_string()
470                    } else {
471                        "missing".to_string()
472                    },
473                    accel_graph_source: runtime_graph_source.as_str().to_string(),
474                    mir_local_fact_count: mir_local_fact_count_for_entrypoint(
475                        &analysis,
476                        &lowering.assembly,
477                    ),
478                    mir_diagnostic_count: analysis.diagnostics.len(),
479                    mir_fusion_signal_count: bytecode.fusion_metadata.mir_fusion_signal_count,
480                    mir_fusion_candidate_group_count: bytecode
481                        .fusion_metadata
482                        .mir_fusion_candidate_group_count,
483                    mir_semantic_instruction_window_count: bytecode
484                        .fusion_metadata
485                        .instruction_window_count,
486                }),
487            )
488        } else {
489            None
490        };
491        #[cfg(target_arch = "wasm32")]
492        let fusion_snapshot: Option<FusionPlanSnapshot> = None;
493
494        // Prepare variable array with existing values before execution
495        self.prepare_variable_array_for_execution(&bytecode, &execution_vars, debug_trace);
496
497        if self.verbose {
498            debug!(
499                "Variable array after preparation: {:?}",
500                self.variable_array
501            );
502            debug!("Bytecode instructions: {:?}", bytecode.instructions);
503        }
504
505        #[cfg(feature = "jit")]
506        let mut used_jit = false;
507        #[cfg(not(feature = "jit"))]
508        let used_jit = false;
509        #[cfg(feature = "jit")]
510        let mut execution_completed = false;
511        #[cfg(not(feature = "jit"))]
512        let execution_completed = false;
513        let mut result_value: Option<Value> = None; // Always start fresh for each execution
514        let mut suppressed_value: Option<Value> = None; // Track value for type info when suppressed
515        let mut error = None;
516        let mut workspace_updates: Vec<WorkspaceEntry> = Vec::new();
517        let mut workspace_snapshot_force_full = false;
518        let mut ans_update: Option<(usize, Value)> = None;
519
520        // Check if this is an expression statement (ends with Pop)
521        let is_expression_stmt = bytecode
522            .instructions
523            .last()
524            .map(|instr| matches!(instr, runmat_vm::Instr::Pop))
525            .unwrap_or(false);
526
527        // Determine whether the final statement ended with a semicolon by inspecting the raw input.
528        let is_semicolon_suppressed = {
529            let toks = tokenize_detailed(input);
530            toks.into_iter()
531                .rev()
532                .map(|t| t.token)
533                .find(|token| {
534                    !matches!(
535                        token,
536                        LexToken::Newline
537                            | LexToken::LineComment
538                            | LexToken::BlockComment
539                            | LexToken::Section
540                    )
541                })
542                .map(|t| matches!(t, LexToken::Semicolon))
543                .unwrap_or(false)
544        };
545        let final_stmt_emit = display_context.final_stmt_emit;
546
547        if self.verbose {
548            debug!("Semantic entry body len: {stmt_count}");
549            if let Some(stmt) = first_entry_statement(&lowering.assembly) {
550                debug!("Semantic HIR statement: {stmt:?}");
551            }
552            debug!("is_semicolon_suppressed: {is_semicolon_suppressed}");
553        }
554
555        // Use JIT for assignments, interpreter for expressions (to capture results properly)
556        #[cfg(feature = "jit")]
557        {
558            if let Some(ref mut jit_engine) = &mut self.jit_engine {
559                if !is_expression_stmt {
560                    // Ensure variable array is large enough
561                    if self.variable_array.len() < bytecode.var_count {
562                        self.variable_array
563                            .resize(bytecode.var_count, Value::Num(0.0));
564                    }
565
566                    if self.verbose {
567                        debug!(
568                            "JIT path for assignment: variable_array size: {}, bytecode.var_count: {}",
569                            self.variable_array.len(),
570                            bytecode.var_count
571                        );
572                    }
573
574                    // Use JIT for assignments
575                    match jit_engine.execute_or_compile(&bytecode, &mut self.variable_array) {
576                        Ok((_, actual_used_jit)) => {
577                            used_jit = actual_used_jit;
578                            execution_completed = true;
579                            if actual_used_jit {
580                                self.stats.jit_compiled += 1;
581                            } else {
582                                self.stats.interpreter_fallback += 1;
583                            }
584                            if !display_context.single_stmt_non_assign {
585                                if let Some(var_id) = display_context.first_assign_var {
586                                    if let Some(name) = id_to_name.get(&var_id) {
587                                        assigned_this_execution.insert(name.clone());
588                                    }
589                                    if var_id < self.variable_array.len() {
590                                        let assignment_value = self.variable_array[var_id].clone();
591                                        if !is_semicolon_suppressed {
592                                            result_value = Some(assignment_value);
593                                            if self.verbose {
594                                                debug!("JIT assignment result: {result_value:?}");
595                                            }
596                                        } else {
597                                            suppressed_value = Some(assignment_value);
598                                            if self.verbose {
599                                                debug!(
600                                                    "JIT assignment suppressed due to semicolon, captured for type info"
601                                                );
602                                            }
603                                        }
604                                    }
605                                }
606                            }
607
608                            if self.verbose {
609                                debug!(
610                                    "{} assignment successful, variable_array: {:?}",
611                                    if actual_used_jit {
612                                        "JIT"
613                                    } else {
614                                        "Interpreter"
615                                    },
616                                    self.variable_array
617                                );
618                            }
619                        }
620                        Err(e) => {
621                            if self.verbose {
622                                debug!("JIT execution failed: {e}, using interpreter");
623                            }
624                            // Fall back to interpreter
625                        }
626                    }
627                }
628            }
629        }
630
631        // Use interpreter if JIT failed or is disabled
632        if !execution_completed {
633            if self.verbose {
634                debug!(
635                    "Interpreter path: variable_array size: {}, bytecode.var_count: {}",
636                    self.variable_array.len(),
637                    bytecode.var_count
638                );
639            }
640
641            // For expressions, modify bytecode to store result in a temp variable instead of using stack
642            let mut execution_bytecode = bytecode.clone();
643            if is_expression_stmt
644                && matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)
645                && !execution_bytecode.instructions.is_empty()
646            {
647                execution_bytecode.instructions.pop(); // Remove the Pop instruction
648
649                // Add StoreVar instruction to store the result in a temporary variable
650                let temp_var_id = std::cmp::max(execution_bytecode.var_count, max_var_id + 1);
651                execution_bytecode
652                    .instructions
653                    .push(runmat_vm::Instr::StoreVar(temp_var_id));
654                execution_bytecode.var_count = temp_var_id + 1; // Expand variable count for temp variable
655
656                // Ensure our variable array can hold the temporary variable
657                if self.variable_array.len() <= temp_var_id {
658                    self.variable_array.resize(temp_var_id + 1, Value::Num(0.0));
659                }
660
661                if self.verbose {
662                    debug!(
663                        "Modified expression bytecode, new instructions: {:?}",
664                        execution_bytecode.instructions
665                    );
666                }
667            }
668
669            match self.interpret_with_context(&execution_bytecode).await {
670                Ok(runmat_vm::InterpreterOutcome::Completed(results)) => {
671                    // Only increment interpreter_fallback if JIT wasn't attempted
672                    if !self.has_jit() || is_expression_stmt {
673                        self.stats.interpreter_fallback += 1;
674                    }
675                    if self.verbose {
676                        debug!("Interpreter results: {results:?}");
677                    }
678
679                    // Handle assignment statements (x = 42 should show the assigned value unless suppressed)
680                    if stmt_count == 1 {
681                        if !display_context.single_stmt_non_assign {
682                            if let Some(var_id) = display_context.first_assign_var {
683                                if let Some(name) = id_to_name.get(&var_id) {
684                                    assigned_this_execution.insert(name.clone());
685                                }
686                                // For assignments, capture the assigned value for both display and type info
687                                if var_id < self.variable_array.len() {
688                                    let assignment_value = self.variable_array[var_id].clone();
689                                    if !is_semicolon_suppressed {
690                                        result_value = Some(assignment_value);
691                                        if self.verbose {
692                                            debug!(
693                                                "Interpreter assignment result: {result_value:?}"
694                                            );
695                                        }
696                                    } else {
697                                        suppressed_value = Some(assignment_value);
698                                        if self.verbose {
699                                            debug!(
700                                                "Interpreter assignment suppressed due to semicolon, captured for type info"
701                                            );
702                                        }
703                                    }
704                                }
705                            }
706                        } else if !is_expression_stmt
707                            && !results.is_empty()
708                            && !is_semicolon_suppressed
709                            && !display_context.single_stmt_non_assign
710                            && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
711                        {
712                            result_value = Some(results[0].clone());
713                        }
714                    }
715
716                    // For expressions, get the result from the temporary variable (capture for both display and type info)
717                    if is_expression_stmt
718                        && matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)
719                        && !execution_bytecode.instructions.is_empty()
720                        && result_value.is_none()
721                        && suppressed_value.is_none()
722                    {
723                        let temp_var_id = execution_bytecode.var_count - 1; // The temp variable we added
724                        if temp_var_id < self.variable_array.len() {
725                            let expression_value = self.variable_array[temp_var_id].clone();
726                            if !is_semicolon_suppressed {
727                                // Capture for 'ans' update when output is not suppressed
728                                ans_update = Some((temp_var_id, expression_value.clone()));
729                                result_value = Some(expression_value);
730                                if self.verbose {
731                                    debug!(
732                                        "Expression result from temp var {temp_var_id}: {result_value:?}"
733                                    );
734                                }
735                            } else {
736                                suppressed_value = Some(expression_value);
737                                if self.verbose {
738                                    debug!(
739                                        "Expression suppressed, captured for type info from temp var {temp_var_id}: {suppressed_value:?}"
740                                    );
741                                }
742                            }
743                        }
744                    } else if !is_semicolon_suppressed
745                        && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
746                        && result_value.is_none()
747                    {
748                        result_value = results.into_iter().last();
749                        if self.verbose {
750                            debug!("Fallback result from interpreter: {result_value:?}");
751                        }
752                    }
753
754                    if self.verbose {
755                        debug!("Final result_value: {result_value:?}");
756                    }
757                    debug!("Interpreter execution successful");
758                }
759
760                Err(e) => {
761                    debug!("Interpreter execution failed: {e}");
762                    error = Some(e);
763                }
764            }
765        }
766
767        let last_assign_var = display_context.last_assign_var;
768        let last_expr_emits = display_context.last_expr_emits;
769        if !is_semicolon_suppressed && result_value.is_none() {
770            let can_emit_from_context = !display_var_ids.is_empty() || last_expr_emits;
771            if can_emit_from_context {
772                if let Some(value) = runmat_runtime::console::take_last_value_output() {
773                    result_value = Some(value);
774                }
775                if result_value.is_none() {
776                    if let Some(var_id) = last_store_var_index(&bytecode) {
777                        if var_id < self.variable_array.len() {
778                            result_value = Some(self.variable_array[var_id].clone());
779                        }
780                    }
781                    if result_value.is_none() {
782                        if let Some(var_id) = last_assign_var {
783                            if var_id < self.variable_array.len() {
784                                result_value = Some(self.variable_array[var_id].clone());
785                            }
786                        }
787                    }
788                    if result_value.is_none() {
789                        if let Some(var_id) = last_emit_var_index(&bytecode) {
790                            if var_id < self.variable_array.len() {
791                                result_value = Some(self.variable_array[var_id].clone());
792                            }
793                        }
794                    }
795                }
796            }
797        }
798
799        let execution_time = start_time.elapsed();
800        let execution_time_ms = execution_time.as_millis() as u64;
801
802        self.stats.total_execution_time_ms += execution_time_ms;
803        self.stats.average_execution_time_ms =
804            self.stats.total_execution_time_ms as f64 / self.stats.total_executions as f64;
805
806        // Update variable names mapping and function definitions if execution was successful
807        if error.is_none() {
808            if let Some(workspace_state) = runmat_vm::take_updated_workspace_state() {
809                let mutated_names = workspace_state.names;
810                let assigned = workspace_state.assigned;
811                if debug_trace {
812                    debug!(
813                        ?mutated_names,
814                        ?assigned,
815                        "[repl] mutated names and assigned return values"
816                    );
817                }
818                self.workspace_bindings.clear();
819                for (name, slot) in &mutated_names {
820                    self.bind_workspace_slot(name.clone(), *slot);
821                }
822                let previous_workspace = self.workspace_values.clone();
823                let current_names: HashSet<String> = assigned
824                    .iter()
825                    .filter(|name| {
826                        mutated_names
827                            .get(*name)
828                            .map(|var_id| *var_id < self.variable_array.len())
829                            .unwrap_or(false)
830                    })
831                    .cloned()
832                    .collect();
833                let removed_names: HashSet<String> = previous_workspace
834                    .keys()
835                    .filter(|name| !current_names.contains(*name))
836                    .cloned()
837                    .collect();
838                let mut rebuilt_workspace = HashMap::new();
839                let mut changed_names: HashSet<String> = assigned
840                    .difference(&prev_assigned_snapshot)
841                    .cloned()
842                    .collect();
843
844                for name in &current_names {
845                    let Some(var_id) = mutated_names.get(name).copied() else {
846                        continue;
847                    };
848                    if var_id >= self.variable_array.len() {
849                        continue;
850                    }
851                    let value_clone = self.variable_array[var_id].clone();
852                    if previous_workspace.get(name) != Some(&value_clone) {
853                        changed_names.insert(name.clone());
854                    }
855                    rebuilt_workspace.insert(name.clone(), value_clone);
856                }
857
858                if debug_trace {
859                    debug!(?changed_names, ?removed_names, "[repl] workspace changes");
860                }
861
862                self.workspace_values = rebuilt_workspace;
863                if !removed_names.is_empty() {
864                    workspace_snapshot_force_full = true;
865                } else {
866                    for name in changed_names {
867                        if let Some(value_clone) = self.workspace_values.get(&name).cloned() {
868                            workspace_updates.push(workspace_entry(&name, &value_clone));
869                            if debug_trace {
870                                debug!(name, ?value_clone, "[repl] workspace update");
871                            }
872                        }
873                    }
874                }
875            } else {
876                let previous_workspace = self.workspace_values.clone();
877                let mut rebuilt_workspace = HashMap::new();
878                let mut changed_names: HashSet<String> = HashSet::new();
879
880                for (name, var_id) in &execution_vars {
881                    if *var_id >= self.variable_array.len() {
882                        continue;
883                    }
884                    let value_clone = self.variable_array[*var_id].clone();
885                    if previous_workspace.get(name) != Some(&value_clone) {
886                        changed_names.insert(name.clone());
887                    }
888                    self.bind_workspace_slot(name.clone(), *var_id);
889                    rebuilt_workspace.insert(name.clone(), value_clone);
890                }
891
892                let removed_names: HashSet<String> = previous_workspace
893                    .keys()
894                    .filter(|name| !rebuilt_workspace.contains_key(*name))
895                    .cloned()
896                    .collect();
897
898                self.workspace_values = rebuilt_workspace;
899                if !removed_names.is_empty() {
900                    workspace_snapshot_force_full = true;
901                } else {
902                    for name in changed_names {
903                        if let Some(value_clone) = self.workspace_values.get(&name).cloned() {
904                            workspace_updates.push(workspace_entry(&name, &value_clone));
905                        }
906                    }
907                }
908            }
909            self.function_registry = function_registry_after_success;
910            self.next_semantic_function_id = next_semantic_function_id_after_success;
911            // Apply 'ans' update if applicable (persisting expression result)
912            if let Some((var_id, value)) = ans_update {
913                self.bind_workspace_slot("ans".to_string(), var_id);
914                self.workspace_values.insert("ans".to_string(), value);
915                if debug_trace {
916                    println!("Updated 'ans' to var_id {}", var_id);
917                }
918            }
919        }
920
921        if self.verbose {
922            debug!("Execution completed in {execution_time_ms}ms (JIT: {used_jit})");
923        }
924
925        if !is_expression_stmt
926            && !is_semicolon_suppressed
927            && last_assign_var.is_some()
928            && !display_context.single_stmt_non_assign
929            && !display_var_ids.is_empty()
930        {
931            if let Some(var_id) = last_store_var_index(&bytecode) {
932                if var_id < self.variable_array.len() {
933                    result_value = Some(self.variable_array[var_id].clone());
934                }
935            } else if matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
936                && result_value.is_none()
937            {
938                if let Some(v) = self
939                    .variable_array
940                    .iter()
941                    .rev()
942                    .find(|v| !matches!(v, Value::Num(0.0)))
943                    .cloned()
944                {
945                    result_value = Some(v);
946                }
947            }
948        }
949
950        if !is_semicolon_suppressed
951            && (!display_var_ids.is_empty()
952                || matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
953                || display_context.single_assign_var.is_some()
954                || (is_expression_stmt
955                    && matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)))
956            && runmat_runtime::console::take_last_value_output().is_none()
957        {
958            if display_var_ids.is_empty() {
959                if let Some(value) = result_value.as_ref() {
960                    let label = last_emit_var_index(&bytecode)
961                        .and_then(|var_id| id_to_name.get(&var_id).cloned())
962                        .or_else(|| {
963                            determine_display_label_from_context(
964                                display_context.single_assign_var,
965                                &id_to_name,
966                                is_expression_stmt,
967                                display_context.single_stmt_non_assign,
968                            )
969                        });
970                    runmat_runtime::console::record_value_output(label.as_deref(), value);
971                }
972            } else {
973                for var_id in display_var_ids {
974                    if let (Some(label), Some(display_value)) =
975                        (id_to_name.get(&var_id), self.variable_array.get(var_id))
976                    {
977                        runmat_runtime::console::record_value_output(
978                            Some(label.as_str()),
979                            display_value,
980                        );
981                    }
982                }
983            }
984        }
985
986        // Generate type info if we have a suppressed value
987        let type_info = suppressed_value.as_ref().map(format_type_info);
988
989        let streams = runmat_runtime::console::take_thread_buffer()
990            .into_iter()
991            .map(|entry| ExecutionStreamEntry {
992                stream: match entry.stream {
993                    runmat_runtime::console::ConsoleStream::Stdout => ExecutionStreamKind::Stdout,
994                    runmat_runtime::console::ConsoleStream::Stderr => ExecutionStreamKind::Stderr,
995                    runmat_runtime::console::ConsoleStream::ClearScreen => {
996                        ExecutionStreamKind::ClearScreen
997                    }
998                },
999                text: entry.text,
1000                timestamp_ms: entry.timestamp_ms,
1001            })
1002            .collect();
1003        let (workspace_entries, snapshot_full) = if workspace_snapshot_force_full {
1004            let mut entries: Vec<WorkspaceEntry> = self
1005                .workspace_values
1006                .iter()
1007                .map(|(name, value)| workspace_entry(name, value))
1008                .collect();
1009            entries.sort_by(|a, b| a.name.cmp(&b.name));
1010            (entries, true)
1011        } else if workspace_updates.is_empty() {
1012            if self.workspace_values.is_empty() {
1013                (workspace_updates, false)
1014            } else {
1015                let mut entries: Vec<WorkspaceEntry> = self
1016                    .workspace_values
1017                    .iter()
1018                    .map(|(name, value)| workspace_entry(name, value))
1019                    .collect();
1020                entries.sort_by(|a, b| a.name.cmp(&b.name));
1021                (entries, true)
1022            }
1023        } else {
1024            (workspace_updates, false)
1025        };
1026        let workspace_snapshot = self.build_workspace_snapshot(workspace_entries, snapshot_full);
1027        let figures_touched = runmat_runtime::plotting_hooks::take_recent_figures();
1028        let stdin_events = stdin_events
1029            .lock()
1030            .map(|guard| guard.clone())
1031            .unwrap_or_default();
1032
1033        let warnings = runmat_runtime::warning_store::take_all();
1034
1035        if let Some(runtime_error) = &mut error {
1036            self.normalize_error_namespace(runtime_error);
1037            self.populate_callstack(runtime_error);
1038        }
1039
1040        let suppress_public_value =
1041            is_expression_stmt && matches!(final_stmt_emit, FinalStmtEmitDisposition::Suppressed);
1042        let public_value = if is_semicolon_suppressed || suppress_public_value {
1043            None
1044        } else {
1045            result_value
1046        };
1047
1048        let mut diagnostics = Vec::new();
1049        if let Some(error) = &error {
1050            diagnostics.push(crate::abi::RuntimeDiagnostic {
1051                code: error
1052                    .identifier()
1053                    .unwrap_or("RunMat:RuntimeError")
1054                    .to_string(),
1055                severity: crate::abi::DiagnosticSeverity::Error,
1056                message: error.message().to_string(),
1057                span: runtime_error_span(error),
1058                callstack: if !error.context.call_stack.is_empty() {
1059                    error.context.call_stack.clone()
1060                } else {
1061                    error
1062                        .context
1063                        .call_frames
1064                        .iter()
1065                        .map(|frame| frame.function.clone())
1066                        .collect()
1067                },
1068                callstack_elided: error.context.call_frames_elided,
1069            });
1070        }
1071        diagnostics.extend(
1072            warnings
1073                .iter()
1074                .map(|warning| crate::abi::RuntimeDiagnostic {
1075                    code: warning.identifier.clone(),
1076                    severity: crate::abi::DiagnosticSeverity::Warning,
1077                    message: warning.message.clone(),
1078                    span: None,
1079                    callstack: Vec::new(),
1080                    callstack_elided: 0,
1081                }),
1082        );
1083
1084        let display_events = public_value
1085            .as_ref()
1086            .map(|value| crate::abi::DisplayEvent {
1087                label: crate::abi::DisplayLabel::Anonymous,
1088                value: value.clone(),
1089                span: runmat_hir::Span::default(),
1090            })
1091            .into_iter()
1092            .collect();
1093
1094        let profiling = gather_profiling(execution_time_ms);
1095        let outcome = crate::abi::ExecutionOutcome {
1096            flow: public_value
1097                .clone()
1098                .map(crate::abi::RuntimeFlow::Single)
1099                .unwrap_or(crate::abi::RuntimeFlow::NoValue),
1100            workspace_delta: crate::abi::WorkspaceDelta {
1101                version: workspace_snapshot.version,
1102                full_snapshot_required: workspace_snapshot.full,
1103                ..crate::abi::WorkspaceDelta::default()
1104            },
1105            display_events,
1106            streams,
1107            diagnostics,
1108            effects: Vec::new(),
1109            suspension: None,
1110            execution_time_ms,
1111            used_jit,
1112            type_info,
1113            figures_touched,
1114            stdin_events,
1115            fusion_plan: fusion_snapshot,
1116            profiling,
1117        };
1118
1119        self.format_mode = runmat_builtins::get_display_format();
1120        Ok(SessionExecution {
1121            outcome,
1122            workspace_snapshot,
1123        })
1124    }
1125
1126    /// Interpret bytecode with persistent variable context
1127    async fn interpret_with_context(
1128        &mut self,
1129        bytecode: &runmat_vm::Bytecode,
1130    ) -> Result<runmat_vm::InterpreterOutcome, RuntimeError> {
1131        let source_name = self.current_source_name().to_string();
1132        runmat_vm::interpret_with_vars(
1133            bytecode,
1134            &mut self.variable_array,
1135            Some(source_name.as_str()),
1136        )
1137        .await
1138    }
1139
1140    fn abi_workspace_upserts(
1141        &self,
1142        workspace_names: Vec<String>,
1143    ) -> Vec<crate::abi::WorkspaceBindingValue> {
1144        let mut workspace_names = workspace_names;
1145        workspace_names.sort();
1146        workspace_names.dedup();
1147        workspace_names
1148            .into_iter()
1149            .filter_map(|name| {
1150                let value = self.workspace_values.get(&name)?.clone();
1151                let binding = runmat_hir::BindingName(name);
1152                let key = self
1153                    .workspace_bindings
1154                    .get(&binding.0)
1155                    .map(|binding| binding.key.clone())
1156                    .unwrap_or_else(|| self.workspace_binding_key(&binding.0));
1157                Some(crate::abi::WorkspaceBindingValue { key, value })
1158            })
1159            .collect()
1160    }
1161
1162    fn abi_workspace_removals(
1163        &self,
1164        previous_workspace_names: HashSet<String>,
1165    ) -> Vec<crate::abi::WorkspaceBindingKey> {
1166        let mut removed_names = previous_workspace_names
1167            .into_iter()
1168            .filter(|name| !self.workspace_values.contains_key(name))
1169            .collect::<Vec<_>>();
1170        removed_names.sort();
1171        removed_names
1172            .into_iter()
1173            .map(|name| self.workspace_binding_key(&name))
1174            .collect()
1175    }
1176}
1177
1178fn apply_requested_output_policy(
1179    mut outcome: crate::abi::ExecutionOutcome,
1180    requested_outputs: &runmat_hir::RequestedOutputCount,
1181) -> crate::abi::ExecutionOutcome {
1182    use crate::abi::RuntimeFlow;
1183    use runmat_hir::RequestedOutputCount;
1184
1185    outcome.flow = match requested_outputs {
1186        RequestedOutputCount::Zero => RuntimeFlow::NoValue,
1187        RequestedOutputCount::One => match outcome.flow {
1188            RuntimeFlow::OutputList(mut values) | RuntimeFlow::CommaList(mut values) => {
1189                if values.is_empty() {
1190                    RuntimeFlow::NoValue
1191                } else {
1192                    RuntimeFlow::Single(values.remove(0))
1193                }
1194            }
1195            flow => flow,
1196        },
1197        RequestedOutputCount::Exactly(count) => {
1198            if *count == 0 {
1199                RuntimeFlow::NoValue
1200            } else if *count == 1 {
1201                match outcome.flow {
1202                    RuntimeFlow::OutputList(mut values) | RuntimeFlow::CommaList(mut values) => {
1203                        if values.is_empty() {
1204                            RuntimeFlow::NoValue
1205                        } else {
1206                            RuntimeFlow::Single(values.remove(0))
1207                        }
1208                    }
1209                    flow => flow,
1210                }
1211            } else {
1212                match outcome.flow {
1213                    RuntimeFlow::NoValue => RuntimeFlow::OutputList(Vec::new()),
1214                    RuntimeFlow::Single(value) => RuntimeFlow::OutputList(vec![value]),
1215                    RuntimeFlow::OutputList(mut values) | RuntimeFlow::CommaList(mut values) => {
1216                        values.truncate(*count);
1217                        RuntimeFlow::OutputList(values)
1218                    }
1219                    RuntimeFlow::DynamicList(handle) => RuntimeFlow::DynamicList(handle),
1220                }
1221            }
1222        }
1223        RequestedOutputCount::CurrentFunctionNargout => outcome.flow,
1224    };
1225    outcome
1226}
1227
1228fn resolve_source_identity(
1229    source: &crate::abi::SourceInput,
1230    source_text: &str,
1231) -> Option<crate::abi::SourceIdentity> {
1232    match source {
1233        crate::abi::SourceInput::Path(path) => {
1234            Some(crate::abi::SourceIdentity::PathAndContentHash {
1235                path: path.clone(),
1236                hash: source_text_hash(source_text),
1237            })
1238        }
1239        crate::abi::SourceInput::Text { name, .. } => {
1240            if name.starts_with('<') {
1241                None
1242            } else {
1243                Some(crate::abi::SourceIdentity::PathAndContentHash {
1244                    path: name.clone(),
1245                    hash: source_text_hash(source_text),
1246                })
1247            }
1248        }
1249    }
1250}
1251
1252fn source_text_hash(source_text: &str) -> String {
1253    use std::hash::{Hash, Hasher};
1254    let mut hasher = std::collections::hash_map::DefaultHasher::new();
1255    source_text.hash(&mut hasher);
1256    format!("{:016x}", hasher.finish())
1257}
1258
1259fn unresolved_source_context(
1260    source: &crate::abi::SourceInput,
1261) -> crate::abi::ExecutionSourceContext {
1262    let name = match source {
1263        crate::abi::SourceInput::Path(path) => path.clone(),
1264        crate::abi::SourceInput::Text { name, .. } => name.clone(),
1265    };
1266    crate::abi::ExecutionSourceContext {
1267        name,
1268        text: match source {
1269            crate::abi::SourceInput::Text { text, .. } => Some(text.clone()),
1270            crate::abi::SourceInput::Path(_) => None,
1271        },
1272        identity: None,
1273    }
1274}
1275
1276fn runtime_error_span(error: &runmat_runtime::RuntimeError) -> Option<runmat_hir::Span> {
1277    error.span.as_ref().map(|span| {
1278        let start = span.offset();
1279        runmat_hir::Span {
1280            start,
1281            end: start + span.len().max(1),
1282        }
1283    })
1284}
1285
1286fn compile_eval_hook_bytecode(
1287    lowering: &runmat_hir::LoweringResult,
1288) -> Result<runmat_vm::Bytecode, runmat_vm::CompileError> {
1289    let entrypoint = lowering.assembly.entrypoints.first().ok_or_else(|| {
1290        runmat_vm::CompileError::new("semantic eval hook compile requires an entrypoint")
1291    })?;
1292    let mir = runmat_mir::lowering::lower_assembly(&lowering.assembly)
1293        .map_err(runmat_vm::CompileError::from)?;
1294    let _analysis = runmat_mir::analysis::analyze_assembly(&mir);
1295    runmat_vm::compile(&lowering.assembly, &mir, entrypoint.id)
1296}
1297
1298fn execution_workspace_mapping(bytecode: &runmat_vm::Bytecode) -> HashMap<String, usize> {
1299    let Some(layout) = &bytecode.layout else {
1300        return HashMap::new();
1301    };
1302    let mut mapping = HashMap::new();
1303    for entrypoint in layout.entrypoints.values() {
1304        for export in &entrypoint.exports {
1305            mapping.insert(export.name.clone(), export.slot.0);
1306        }
1307    }
1308    mapping
1309}
1310
1311fn entry_function(assembly: &runmat_hir::HirAssembly) -> Option<&runmat_hir::HirFunction> {
1312    let entrypoint = assembly.entrypoints.first()?;
1313    assembly
1314        .functions
1315        .iter()
1316        .find(|function| function.id == entrypoint.target)
1317}
1318
1319fn entry_statement_count(assembly: &runmat_hir::HirAssembly) -> usize {
1320    entry_function(assembly)
1321        .map(|function| function.body.statements.len())
1322        .unwrap_or(0)
1323}
1324
1325fn first_entry_statement(assembly: &runmat_hir::HirAssembly) -> Option<&runmat_hir::HirStmt> {
1326    entry_function(assembly)?.body.statements.first()
1327}
1328
1329struct SessionExecution {
1330    outcome: crate::abi::ExecutionOutcome,
1331    workspace_snapshot: WorkspaceSnapshot,
1332}
1333
1334async fn source_input_text(
1335    source: crate::abi::SourceInput,
1336) -> std::result::Result<(String, String), RunError> {
1337    match source {
1338        crate::abi::SourceInput::Text { name, text } => Ok((name, text)),
1339        crate::abi::SourceInput::Path(path) => {
1340            let source_path = resolve_path_source_input(&path).await?;
1341
1342            let text = runmat_filesystem::read_to_string_async(&source_path)
1343                .await
1344                .map_err(|err| {
1345                    RunError::Runtime(
1346                        build_runtime_error(format!(
1347                            "failed to read source path '{}': {err}",
1348                            source_path.display()
1349                        ))
1350                        .with_identifier("RunMat:SourceReadFailed")
1351                        .build(),
1352                    )
1353                })?;
1354            Ok((source_path.to_string_lossy().to_string(), text))
1355        }
1356    }
1357}
1358
1359async fn resolve_path_source_input(
1360    path: &str,
1361) -> std::result::Result<std::path::PathBuf, RunError> {
1362    #[cfg(not(target_arch = "wasm32"))]
1363    {
1364        use runmat_config::project::resolve_project_source_input_from;
1365        use std::path::Path;
1366
1367        let cwd = runmat_filesystem::current_dir().map_err(|err| {
1368        RunError::Runtime(
1369            build_runtime_error(format!(
1370                "failed to resolve current working directory while resolving source path '{path}': {err}"
1371            ))
1372            .with_identifier("RunMat:SourceResolveFailed")
1373            .build(),
1374        )
1375    })?;
1376
1377        let resolved = resolve_project_source_input_from(&cwd, Path::new(path)).map_err(|err| {
1378            RunError::Runtime(
1379                build_runtime_error(format!(
1380                    "failed to resolve source input '{}' from working directory {}: {}",
1381                    path,
1382                    cwd.display(),
1383                    err
1384                ))
1385                .with_identifier("RunMat:EntrypointResolveFailed")
1386                .build(),
1387            )
1388        })?;
1389        Ok(if resolved.is_absolute() {
1390            resolved
1391        } else {
1392            cwd.join(resolved)
1393        })
1394    }
1395
1396    #[cfg(target_arch = "wasm32")]
1397    {
1398        use std::path::PathBuf;
1399
1400        let cwd = runmat_filesystem::current_dir().map_err(|err| {
1401            RunError::Runtime(
1402                build_runtime_error(format!(
1403                    "failed to resolve current working directory while resolving source path '{path}': {err}"
1404                ))
1405                .with_identifier("RunMat:SourceResolveFailed")
1406                .build(),
1407            )
1408        })?;
1409        let source_path = PathBuf::from(path);
1410        let candidate = if source_path.is_absolute() {
1411            source_path.clone()
1412        } else {
1413            cwd.join(&source_path)
1414        };
1415
1416        if let Ok(metadata) = runmat_filesystem::metadata_async(&candidate).await {
1417            if metadata.is_file() {
1418                return Ok(candidate);
1419            }
1420        }
1421
1422        if source_path.extension().is_none() {
1423            let with_ext = candidate.with_extension("m");
1424            if let Ok(metadata) = runmat_filesystem::metadata_async(&with_ext).await {
1425                if metadata.is_file() {
1426                    return Ok(with_ext);
1427                }
1428            }
1429        }
1430
1431        Ok(candidate)
1432    }
1433}
1434
1435#[cfg(test)]
1436mod tests {
1437    #[cfg(not(target_arch = "wasm32"))]
1438    use super::discover_known_project_symbols;
1439    #[cfg(not(target_arch = "wasm32"))]
1440    use super::source_input_text;
1441    #[cfg(not(target_arch = "wasm32"))]
1442    use crate::abi::SourceInput;
1443    #[cfg(not(target_arch = "wasm32"))]
1444    use crate::RunError;
1445    #[cfg(not(target_arch = "wasm32"))]
1446    use std::fs;
1447    #[cfg(not(target_arch = "wasm32"))]
1448    use std::path::{Path, PathBuf};
1449    #[cfg(not(target_arch = "wasm32"))]
1450    use std::sync::Mutex;
1451
1452    #[cfg(not(target_arch = "wasm32"))]
1453    static CWD_LOCK: Mutex<()> = Mutex::new(());
1454
1455    #[cfg(not(target_arch = "wasm32"))]
1456    struct CwdGuard {
1457        original: PathBuf,
1458    }
1459
1460    #[cfg(not(target_arch = "wasm32"))]
1461    impl Drop for CwdGuard {
1462        fn drop(&mut self) {
1463            let _ = std::env::set_current_dir(&self.original);
1464        }
1465    }
1466
1467    #[cfg(not(target_arch = "wasm32"))]
1468    fn push_cwd(path: &Path) -> CwdGuard {
1469        let original = std::env::current_dir().expect("read cwd");
1470        std::env::set_current_dir(path).expect("set cwd");
1471        CwdGuard { original }
1472    }
1473
1474    #[test]
1475    #[cfg(not(target_arch = "wasm32"))]
1476    fn source_input_path_resolves_named_manifest_entrypoint() {
1477        let _guard = CWD_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
1478        let tmp = tempfile::TempDir::new().unwrap();
1479        fs::create_dir_all(tmp.path().join("src")).unwrap();
1480        fs::write(tmp.path().join("src/main.m"), "x = 1;").unwrap();
1481        fs::write(
1482            tmp.path().join("runmat.toml"),
1483            r#"
1484[package]
1485name = "demo"
1486
1487[sources]
1488roots = ["src"]
1489
1490[entrypoints.main]
1491path = "src/main"
1492"#,
1493        )
1494        .unwrap();
1495        let _cwd = push_cwd(tmp.path());
1496        let (source_name, source_text) =
1497            futures::executor::block_on(source_input_text(SourceInput::Path("main".to_string())))
1498                .expect("named entrypoint should resolve");
1499        let resolved = std::path::PathBuf::from(source_name)
1500            .canonicalize()
1501            .unwrap();
1502        let expected = tmp.path().join("src/main.m").canonicalize().unwrap();
1503        assert_eq!(
1504            resolved, expected,
1505            "resolved source path should match manifest entrypoint target"
1506        );
1507        assert_eq!(source_text, "x = 1;");
1508    }
1509
1510    #[test]
1511    #[cfg(not(target_arch = "wasm32"))]
1512    fn source_input_path_infers_m_extension_for_relative_path() {
1513        let _guard = CWD_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
1514        let tmp = tempfile::TempDir::new().unwrap();
1515        fs::create_dir_all(tmp.path().join("src")).unwrap();
1516        fs::write(tmp.path().join("src/main.m"), "x = 1;").unwrap();
1517        let _cwd = push_cwd(tmp.path());
1518
1519        let (source_name, source_text) = futures::executor::block_on(source_input_text(
1520            SourceInput::Path("src/main".to_string()),
1521        ))
1522        .expect("path without extension should infer .m");
1523
1524        assert!(source_name.ends_with("src/main.m"));
1525        assert_eq!(source_text.trim(), "x = 1;");
1526    }
1527
1528    #[test]
1529    #[cfg(not(target_arch = "wasm32"))]
1530    fn source_input_path_errors_for_invalid_named_entrypoint_target() {
1531        let _guard = CWD_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
1532        let tmp = tempfile::TempDir::new().unwrap();
1533        fs::create_dir_all(tmp.path().join("src")).unwrap();
1534        fs::write(
1535            tmp.path().join("runmat.toml"),
1536            r#"
1537[package]
1538name = "demo"
1539
1540[sources]
1541roots = ["src"]
1542
1543[entrypoints.server]
1544module = "app.server"
1545function = "main"
1546"#,
1547        )
1548        .unwrap();
1549        let _cwd = push_cwd(tmp.path());
1550        let err =
1551            futures::executor::block_on(source_input_text(SourceInput::Path("server".to_string())))
1552                .expect_err("invalid module/function entrypoint should report resolve error");
1553        let RunError::Runtime(runtime_err) = err else {
1554            panic!("expected runtime error");
1555        };
1556        assert_eq!(
1557            runtime_err.identifier.as_deref(),
1558            Some("RunMat:EntrypointResolveFailed")
1559        );
1560    }
1561
1562    #[test]
1563    #[cfg(not(target_arch = "wasm32"))]
1564    fn discover_known_project_symbols_reads_manifest_source_context() {
1565        let _guard = CWD_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
1566        let tmp = tempfile::TempDir::new().unwrap();
1567        fs::create_dir_all(tmp.path().join("+stats")).unwrap();
1568        fs::write(
1569            tmp.path().join("runmat.toml"),
1570            r#"
1571[package]
1572name = "demo"
1573
1574[sources]
1575roots = ["."]
1576"#,
1577        )
1578        .unwrap();
1579        fs::write(
1580            tmp.path().join("+stats/summarize.m"),
1581            "function y = summarize(x); y = x; end",
1582        )
1583        .unwrap();
1584        fs::write(tmp.path().join("main.m"), "x = 1;").unwrap();
1585        let _cwd = push_cwd(tmp.path());
1586
1587        let symbols = discover_known_project_symbols(Some(
1588            tmp.path().join("main.m").to_string_lossy().as_ref(),
1589        ));
1590        assert!(
1591            symbols.contains("stats.summarize"),
1592            "source-context discovery should include project symbols for eval-hook lowering"
1593        );
1594    }
1595
1596    #[test]
1597    #[cfg(not(target_arch = "wasm32"))]
1598    fn discover_known_project_symbols_includes_dependency_alias_qualified_names() {
1599        let _guard = CWD_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
1600        let tmp = tempfile::TempDir::new().unwrap();
1601        let dep_root = tmp.path().join("deps/statslib");
1602        fs::create_dir_all(&dep_root).unwrap();
1603        fs::write(
1604            tmp.path().join("runmat.toml"),
1605            r#"
1606[package]
1607name = "demo"
1608
1609[sources]
1610roots = ["."]
1611
1612[dependencies]
1613statsdep = { path = "deps/statslib" }
1614"#,
1615        )
1616        .unwrap();
1617        fs::write(
1618            dep_root.join("runmat.toml"),
1619            r#"
1620[package]
1621name = "statslib"
1622
1623[sources]
1624roots = ["."]
1625"#,
1626        )
1627        .unwrap();
1628        fs::write(
1629            dep_root.join("summarize.m"),
1630            "function y = summarize(x); y = x; end",
1631        )
1632        .unwrap();
1633        fs::write(tmp.path().join("main.m"), "x = 1;").unwrap();
1634        let _cwd = push_cwd(tmp.path());
1635
1636        let symbols = discover_known_project_symbols(Some(
1637            tmp.path().join("main.m").to_string_lossy().as_ref(),
1638        ));
1639        assert!(
1640            symbols.contains("summarize"),
1641            "expected base dependency symbol in known-project discovery"
1642        );
1643        assert!(
1644            symbols.contains("statslib.summarize"),
1645            "expected package-qualified dependency symbol in known-project discovery"
1646        );
1647        assert!(
1648            symbols.contains("statsdep.summarize"),
1649            "expected dependency-alias-qualified symbol in known-project discovery"
1650        );
1651    }
1652}