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