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