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