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