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