Skip to main content

runmat_core/session/
run.rs

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