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(®istry),
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 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 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 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 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; let mut suppressed_value: Option<Value> = None; 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 let is_expression_stmt = bytecode
779 .instructions
780 .last()
781 .map(|instr| matches!(instr, runmat_vm::Instr::Pop))
782 .unwrap_or(false);
783
784 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 #[cfg(feature = "jit")]
814 {
815 if let Some(ref mut jit_engine) = &mut self.jit_engine {
816 if !is_expression_stmt {
817 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 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 }
883 }
884 }
885 }
886 }
887
888 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 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(); 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; 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 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 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 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 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; 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 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 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 ¤t_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 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 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 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}