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