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