use super::*;
#[cfg(not(target_arch = "wasm32"))]
fn entrypoint_target_function(
assembly: &runmat_hir::HirAssembly,
) -> Option<runmat_hir::FunctionId> {
assembly
.entrypoints
.first()
.map(|entrypoint| entrypoint.target)
}
#[cfg(not(target_arch = "wasm32"))]
fn mir_local_fact_count_for_entrypoint(
analysis: &runmat_mir::analysis::AnalysisStore,
assembly: &runmat_hir::HirAssembly,
) -> usize {
let Some(entrypoint_target) = entrypoint_target_function(assembly) else {
return analysis.mir_locals.len();
};
analysis
.mir_locals
.keys()
.filter(|key| key.function == entrypoint_target)
.count()
}
fn discover_known_project_symbols(source_name: Option<&str>) -> HashSet<String> {
use runmat_config::project::discover_known_project_symbols_from_source_name;
use std::path::{Path, PathBuf};
let Ok(cwd) = runmat_filesystem::current_dir() else {
let Some(source_name) = source_name else {
return HashSet::new();
};
let source_path = PathBuf::from(source_name);
if source_path.is_absolute() {
let source_cwd = source_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("/"));
return discover_known_project_symbols_from_source_name(Some(source_name), &source_cwd);
}
return HashSet::new();
};
discover_known_project_symbols_from_source_name(source_name, &cwd)
}
impl RunMatSession {
async fn run(
&mut self,
input: &str,
) -> std::result::Result<crate::abi::ExecutionOutcome, RunError> {
let companion = super::compile::discover_companion_source_statements_async(
self.current_source_name(),
self.compat_mode,
)
.await;
self.pending_companion_source_discovery = Some(companion);
let previous_workspace_names = self
.workspace_values
.keys()
.cloned()
.collect::<HashSet<_>>();
let mut execution = self.execute_internal(input, true).await?;
let workspace_names = execution
.workspace_snapshot
.values
.iter()
.map(|entry| entry.name.clone())
.collect::<Vec<_>>();
let workspace_full = execution.workspace_snapshot.full;
let outcome = &mut execution.outcome;
outcome.workspace_delta.upserts = self.abi_workspace_upserts(workspace_names);
if workspace_full {
outcome.workspace_delta.removals =
self.abi_workspace_removals(previous_workspace_names);
if !outcome.workspace_delta.removals.is_empty() {
outcome.effects.push(crate::abi::ObservedEffect::Workspace(
crate::abi::WorkspaceEffectKind::Clear,
));
}
}
Ok(execution.outcome)
}
pub async fn execute_request(
&mut self,
request: crate::abi::ExecutionRequest,
) -> std::result::Result<crate::abi::ExecutionOutcome, RunError> {
let requested_outputs = request.requested_outputs.clone();
let source_input = request.source.clone();
let (source_name, source_text) = source_input_text(request.source).await?;
let previous_compat = self.compat_mode;
let previous_top_level_await_enabled = self.top_level_await_enabled;
let previous_dynamic_eval_enabled = self.dynamic_eval_enabled;
let previous_source_name = self.active_source_name.clone();
let previous_workspace_handle = self.abi_workspace_handle;
let previous_source_identity = self.active_source_identity.clone();
self.compat_mode = request.compatibility;
self.top_level_await_enabled = request.host_policy.top_level_await;
self.dynamic_eval_enabled = request.host_policy.dynamic_eval;
self.active_source_name = source_name;
self.abi_workspace_handle = request.workspace;
self.active_source_identity = resolve_source_identity(&source_input, &source_text);
let result = self.run(&source_text).await;
self.compat_mode = previous_compat;
self.top_level_await_enabled = previous_top_level_await_enabled;
self.dynamic_eval_enabled = previous_dynamic_eval_enabled;
self.active_source_name = previous_source_name;
self.abi_workspace_handle = previous_workspace_handle;
self.active_source_identity = previous_source_identity;
self.pending_companion_source_discovery = None;
result.map(|outcome| apply_requested_output_policy(outcome, &requested_outputs))
}
async fn execute_internal(
&mut self,
input: &str,
preserve_layout_var_names: bool,
) -> std::result::Result<SessionExecution, RunError> {
let _active = ActiveExecutionGuard::new(self).map_err(|err| {
RunError::Runtime(
build_runtime_error(err.to_string())
.with_identifier("RunMat:ExecutionAlreadyActive")
.build(),
)
})?;
runmat_vm::set_call_stack_limit(self.callstack_limit);
runmat_vm::set_error_namespace(&self.error_namespace);
runmat_vm::set_dynamic_eval_options(
self.compat_mode,
self.compat_mode.allows_runmat_extensions(),
self.top_level_await_enabled,
self.dynamic_eval_enabled,
);
runmat_hir::set_error_namespace(&self.error_namespace);
let exec_span = info_span!(
"runtime.execute",
input_len = input.len(),
verbose = self.verbose
);
let _exec_guard = exec_span.enter();
runmat_runtime::console::reset_thread_buffer();
runmat_runtime::plotting_hooks::reset_recent_figures();
runmat_runtime::warning_store::reset();
runmat_builtins::set_display_format(self.format_mode);
reset_provider_telemetry();
self.interrupt_flag.store(false, Ordering::Relaxed);
let _interrupt_guard =
runmat_runtime::interrupt::replace_interrupt(Some(self.interrupt_flag.clone()));
let start_time = Instant::now();
self.stats.total_executions += 1;
let debug_trace = std::env::var("RUNMAT_DEBUG_REPL").is_ok();
let stdin_events: Arc<Mutex<Vec<StdinEvent>>> = Arc::new(Mutex::new(Vec::new()));
let host_async_handler = self.async_input_handler.clone();
let stdin_events_async = Arc::clone(&stdin_events);
let runtime_async_handler: Arc<runmat_runtime::interaction::AsyncInteractionHandler> =
Arc::new(
move |prompt: runmat_runtime::interaction::InteractionPromptOwned| {
let request_kind = match prompt.kind {
runmat_runtime::interaction::InteractionKind::Line { echo } => {
InputRequestKind::Line { echo }
}
runmat_runtime::interaction::InteractionKind::KeyPress => {
InputRequestKind::KeyPress
}
};
let request = InputRequest {
prompt: prompt.prompt,
kind: request_kind,
};
let (event_kind, echo_flag) = match &request.kind {
InputRequestKind::Line { echo } => (StdinEventKind::Line, *echo),
InputRequestKind::KeyPress => (StdinEventKind::KeyPress, false),
};
let mut event = StdinEvent {
prompt: request.prompt.clone(),
kind: event_kind,
echo: echo_flag,
value: None,
error: None,
};
let stdin_events_async = Arc::clone(&stdin_events_async);
let host_async_handler = host_async_handler.clone();
Box::pin(async move {
let resp: Result<InputResponse, String> =
if let Some(handler) = host_async_handler {
handler(request).await
} else {
match &request.kind {
InputRequestKind::Line { echo } => {
runmat_runtime::interaction::default_read_line(
&request.prompt,
*echo,
)
.map(InputResponse::Line)
}
InputRequestKind::KeyPress => {
runmat_runtime::interaction::default_wait_for_key(
&request.prompt,
)
.map(|_| InputResponse::KeyPress)
}
}
};
let resp = resp.inspect_err(|err| {
event.error = Some(err.clone());
if let Ok(mut guard) = stdin_events_async.lock() {
guard.push(event.clone());
}
})?;
let interaction_resp = match resp {
InputResponse::Line(value) => {
event.value = Some(value.clone());
if let Ok(mut guard) = stdin_events_async.lock() {
guard.push(event);
}
runmat_runtime::interaction::InteractionResponse::Line(value)
}
InputResponse::KeyPress => {
if let Ok(mut guard) = stdin_events_async.lock() {
guard.push(event);
}
runmat_runtime::interaction::InteractionResponse::KeyPress
}
};
Ok(interaction_resp)
})
},
);
let _async_input_guard =
runmat_runtime::interaction::replace_async_handler(Some(runtime_async_handler));
let compat = self.compat_mode;
let top_level_await_enabled = self.top_level_await_enabled;
let source_name_for_eval_hook = self.current_source_name().to_string();
let known_project_symbols_for_eval_hook = Arc::new(discover_known_project_symbols(Some(
source_name_for_eval_hook.as_str(),
)));
let _eval_hook_guard =
runmat_runtime::interaction::replace_eval_hook(Some(std::sync::Arc::new(
move |expr: String| -> runmat_runtime::interaction::EvalHookFuture {
async fn eval_expr(
expr: String,
compat: runmat_parser::CompatMode,
top_level_await_enabled: bool,
known_project_symbols: Arc<HashSet<String>>,
) -> Result<Value, RuntimeError> {
let wrapped = format!("__runmat_input_result__ = ({expr});");
let ast = parse_with_options(&wrapped, ParserOptions::new(compat))
.map_err(|e| {
build_runtime_error(format!("input: parse error: {e}"))
.with_identifier("RunMat:input:ParseError")
.build()
})?;
let lowering = runmat_hir::lower(
&ast,
&LoweringContext::new(&HashMap::new())
.with_known_project_symbols(&known_project_symbols)
.with_runmat_extensions_enabled(compat.allows_runmat_extensions())
.with_top_level_await_enabled(top_level_await_enabled),
)
.map_err(|e| {
build_runtime_error(format!("input: lowering error: {e}"))
.with_identifier("RunMat:input:LowerError")
.build()
})?;
let bc =
compile_eval_hook_bytecode(&lowering).map_err(RuntimeError::from)?;
let result_idx = bc.var_names.iter().find_map(|(idx, name)| {
(name == "__runmat_input_result__").then_some(*idx)
});
let vars = runmat_vm::interpret(&bc).await?;
result_idx
.and_then(|idx| vars.get(idx).cloned())
.ok_or_else(|| {
build_runtime_error("input: expression produced no value")
.with_identifier("RunMat:input:NoValue")
.build()
})
}
#[cfg(target_arch = "wasm32")]
{
Box::pin(eval_expr(
expr,
compat,
top_level_await_enabled,
Arc::clone(&known_project_symbols_for_eval_hook),
))
}
#[cfg(not(target_arch = "wasm32"))]
{
let (tx, rx) = tokio::sync::oneshot::channel();
let known_project_symbols =
Arc::clone(&known_project_symbols_for_eval_hook);
let spawn_result = std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(move || {
let result = futures::executor::block_on(eval_expr(
expr,
compat,
top_level_await_enabled,
known_project_symbols,
));
let _ = tx.send(result);
});
Box::pin(async move {
spawn_result.map_err(|err| {
build_runtime_error(format!(
"input: failed to spawn eval thread: {err}"
))
.with_identifier("RunMat:input:EvalThreadSpawnFailed")
.build()
})?;
rx.await.unwrap_or_else(|_| {
Err(build_runtime_error("input: eval thread panicked")
.with_identifier("RunMat:input:EvalThreadPanic")
.build())
})
})
}
},
)));
if self.verbose {
debug!("Executing: {}", input.trim());
}
let source_name_for_context = self.current_source_name().to_string();
let _fallback_source_guard = runmat_runtime::source_context::replace_current_source_context(
Some(&source_name_for_context),
Some(input),
);
let PreparedExecution {
ast,
lowering,
analysis,
mut bytecode,
function_registry_after_success,
next_semantic_function_id_after_success,
} = self.compile_input(input)?;
let source_catalog_entries = self
.source_pool
.entries()
.map(|(source_id, source)| {
(source_id, source.name.to_string(), source.text.to_string())
})
.collect::<Vec<_>>();
let _source_catalog_guard =
runmat_runtime::source_context::replace_source_catalog(source_catalog_entries);
let _source_id_guard =
runmat_runtime::source_context::replace_current_source_id(bytecode.source_id);
#[cfg(target_arch = "wasm32")]
let _ = &analysis;
if self.verbose {
debug!("AST: {ast:?}");
}
let display = execution_display_context(&lowering.assembly, bytecode.layout.as_ref());
let display_context = display.context;
let display_var_ids = display.display_var_ids;
let stmt_count = entry_statement_count(&lowering.assembly);
let execution_vars = execution_workspace_mapping(&bytecode);
let max_var_id = execution_vars.values().copied().max().unwrap_or(0);
if debug_trace {
debug!(?execution_vars, "[repl] execution vars");
}
if debug_trace {
debug!(workspace_values_before = ?self.workspace_values, "[repl] workspace snapshot before execution");
}
let id_to_name: HashMap<usize, String> = execution_vars
.iter()
.map(|(name, var_id)| (*var_id, name.clone()))
.collect();
let mut assigned_this_execution: HashSet<String> = HashSet::new();
let assigned_snapshot: HashSet<String> = execution_vars
.keys()
.filter(|name| self.workspace_values.contains_key(name.as_str()))
.cloned()
.collect();
let prev_assigned_snapshot = assigned_snapshot.clone();
if debug_trace {
debug!(?assigned_snapshot, "[repl] assigned snapshot");
}
let _pending_workspace_guard =
runmat_vm::push_pending_workspace(execution_vars.clone(), assigned_snapshot.clone());
if self.verbose {
debug!("HIR generated successfully");
}
if preserve_layout_var_names && bytecode.layout.is_some() {
for (slot, name) in &id_to_name {
bytecode.var_names.insert(*slot, name.clone());
}
} else {
bytecode.var_names = id_to_name.clone();
}
if self.verbose {
debug!(
"Bytecode compiled: {} instructions",
bytecode.instructions.len()
);
}
#[cfg(not(target_arch = "wasm32"))]
let fusion_snapshot = if self.emit_fusion_plan {
let runtime_groups = bytecode.runtime_fusion_groups();
let (runtime_graph, runtime_graph_source) =
bytecode.runtime_accel_graph_for_fusion_with_source(&runtime_groups);
build_fusion_snapshot(
&runtime_groups,
&bytecode.fusion_metadata.mir_fusion_candidate_groups,
&bytecode.fusion_metadata.instruction_windows,
Some(crate::fusion::FusionPlannerMetadata {
source: "semantic-mir-analysis-runtime".to_string(),
accel_graph_state: if runtime_graph.is_some() {
"present".to_string()
} else {
"missing".to_string()
},
accel_graph_source: runtime_graph_source.as_str().to_string(),
mir_local_fact_count: mir_local_fact_count_for_entrypoint(
&analysis,
&lowering.assembly,
),
mir_diagnostic_count: analysis.diagnostics.len(),
mir_fusion_signal_count: bytecode.fusion_metadata.mir_fusion_signal_count,
mir_fusion_candidate_group_count: bytecode
.fusion_metadata
.mir_fusion_candidate_group_count,
mir_semantic_instruction_window_count: bytecode
.fusion_metadata
.instruction_window_count,
}),
)
} else {
None
};
#[cfg(target_arch = "wasm32")]
let fusion_snapshot: Option<FusionPlanSnapshot> = None;
self.prepare_variable_array_for_execution(&bytecode, &execution_vars, debug_trace);
if self.verbose {
debug!(
"Variable array after preparation: {:?}",
self.variable_array
);
debug!("Bytecode instructions: {:?}", bytecode.instructions);
}
#[cfg(feature = "jit")]
let mut used_jit = false;
#[cfg(not(feature = "jit"))]
let used_jit = false;
#[cfg(feature = "jit")]
let mut execution_completed = false;
#[cfg(not(feature = "jit"))]
let execution_completed = false;
let mut result_value: Option<Value> = None; let mut suppressed_value: Option<Value> = None; let mut error = None;
let mut workspace_updates: Vec<WorkspaceEntry> = Vec::new();
let mut workspace_snapshot_force_full = false;
let mut ans_update: Option<(usize, Value)> = None;
let is_expression_stmt = bytecode
.instructions
.last()
.map(|instr| matches!(instr, runmat_vm::Instr::Pop))
.unwrap_or(false);
let is_semicolon_suppressed = {
let toks = tokenize_detailed(input);
toks.into_iter()
.rev()
.map(|t| t.token)
.find(|token| {
!matches!(
token,
LexToken::Newline
| LexToken::LineComment
| LexToken::BlockComment
| LexToken::Section
)
})
.map(|t| matches!(t, LexToken::Semicolon))
.unwrap_or(false)
};
let final_stmt_emit = display_context.final_stmt_emit;
if self.verbose {
debug!("Semantic entry body len: {stmt_count}");
if let Some(stmt) = first_entry_statement(&lowering.assembly) {
debug!("Semantic HIR statement: {stmt:?}");
}
debug!("is_semicolon_suppressed: {is_semicolon_suppressed}");
}
#[cfg(feature = "jit")]
{
if let Some(ref mut jit_engine) = &mut self.jit_engine {
if !is_expression_stmt {
if self.variable_array.len() < bytecode.var_count {
self.variable_array
.resize(bytecode.var_count, Value::Num(0.0));
}
if self.verbose {
debug!(
"JIT path for assignment: variable_array size: {}, bytecode.var_count: {}",
self.variable_array.len(),
bytecode.var_count
);
}
match jit_engine.execute_or_compile(&bytecode, &mut self.variable_array) {
Ok((_, actual_used_jit)) => {
used_jit = actual_used_jit;
execution_completed = true;
if actual_used_jit {
self.stats.jit_compiled += 1;
} else {
self.stats.interpreter_fallback += 1;
}
if !display_context.single_stmt_non_assign {
if let Some(var_id) = display_context.first_assign_var {
if let Some(name) = id_to_name.get(&var_id) {
assigned_this_execution.insert(name.clone());
}
if var_id < self.variable_array.len() {
let assignment_value = self.variable_array[var_id].clone();
if !is_semicolon_suppressed {
result_value = Some(assignment_value);
if self.verbose {
debug!("JIT assignment result: {result_value:?}");
}
} else {
suppressed_value = Some(assignment_value);
if self.verbose {
debug!(
"JIT assignment suppressed due to semicolon, captured for type info"
);
}
}
}
}
}
if self.verbose {
debug!(
"{} assignment successful, variable_array: {:?}",
if actual_used_jit {
"JIT"
} else {
"Interpreter"
},
self.variable_array
);
}
}
Err(e) => {
if self.verbose {
debug!("JIT execution failed: {e}, using interpreter");
}
}
}
}
}
}
if !execution_completed {
if self.verbose {
debug!(
"Interpreter path: variable_array size: {}, bytecode.var_count: {}",
self.variable_array.len(),
bytecode.var_count
);
}
let mut execution_bytecode = bytecode.clone();
if is_expression_stmt
&& matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)
&& !execution_bytecode.instructions.is_empty()
{
execution_bytecode.instructions.pop();
let temp_var_id = std::cmp::max(execution_bytecode.var_count, max_var_id + 1);
execution_bytecode
.instructions
.push(runmat_vm::Instr::StoreVar(temp_var_id));
execution_bytecode.var_count = temp_var_id + 1;
if self.variable_array.len() <= temp_var_id {
self.variable_array.resize(temp_var_id + 1, Value::Num(0.0));
}
if self.verbose {
debug!(
"Modified expression bytecode, new instructions: {:?}",
execution_bytecode.instructions
);
}
}
match self.interpret_with_context(&execution_bytecode).await {
Ok(runmat_vm::InterpreterOutcome::Completed(results)) => {
if !self.has_jit() || is_expression_stmt {
self.stats.interpreter_fallback += 1;
}
if self.verbose {
debug!("Interpreter results: {results:?}");
}
if stmt_count == 1 {
if !display_context.single_stmt_non_assign {
if let Some(var_id) = display_context.first_assign_var {
if let Some(name) = id_to_name.get(&var_id) {
assigned_this_execution.insert(name.clone());
}
if var_id < self.variable_array.len() {
let assignment_value = self.variable_array[var_id].clone();
if !is_semicolon_suppressed {
result_value = Some(assignment_value);
if self.verbose {
debug!(
"Interpreter assignment result: {result_value:?}"
);
}
} else {
suppressed_value = Some(assignment_value);
if self.verbose {
debug!(
"Interpreter assignment suppressed due to semicolon, captured for type info"
);
}
}
}
}
} else if !is_expression_stmt
&& !results.is_empty()
&& !is_semicolon_suppressed
&& !display_context.single_stmt_non_assign
&& matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
{
result_value = Some(results[0].clone());
}
}
if is_expression_stmt
&& matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)
&& !execution_bytecode.instructions.is_empty()
&& result_value.is_none()
&& suppressed_value.is_none()
{
let temp_var_id = execution_bytecode.var_count - 1; if temp_var_id < self.variable_array.len() {
let expression_value = self.variable_array[temp_var_id].clone();
if !is_semicolon_suppressed {
ans_update = Some((temp_var_id, expression_value.clone()));
result_value = Some(expression_value);
if self.verbose {
debug!(
"Expression result from temp var {temp_var_id}: {result_value:?}"
);
}
} else {
suppressed_value = Some(expression_value);
if self.verbose {
debug!(
"Expression suppressed, captured for type info from temp var {temp_var_id}: {suppressed_value:?}"
);
}
}
}
} else if !is_semicolon_suppressed
&& matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
&& result_value.is_none()
{
result_value = results.into_iter().last();
if self.verbose {
debug!("Fallback result from interpreter: {result_value:?}");
}
}
if self.verbose {
debug!("Final result_value: {result_value:?}");
}
debug!("Interpreter execution successful");
}
Err(e) => {
debug!("Interpreter execution failed: {e}");
error = Some(e);
}
}
}
let last_assign_var = display_context.last_assign_var;
let last_expr_emits = display_context.last_expr_emits;
if !is_semicolon_suppressed && result_value.is_none() {
let can_emit_from_context = !display_var_ids.is_empty() || last_expr_emits;
if can_emit_from_context {
if let Some(value) = runmat_runtime::console::take_last_value_output() {
result_value = Some(value);
}
if result_value.is_none() {
if let Some(var_id) = last_store_var_index(&bytecode) {
if var_id < self.variable_array.len() {
result_value = Some(self.variable_array[var_id].clone());
}
}
if result_value.is_none() {
if let Some(var_id) = last_assign_var {
if var_id < self.variable_array.len() {
result_value = Some(self.variable_array[var_id].clone());
}
}
}
if result_value.is_none() {
if let Some(var_id) = last_emit_var_index(&bytecode) {
if var_id < self.variable_array.len() {
result_value = Some(self.variable_array[var_id].clone());
}
}
}
}
}
}
let execution_time = start_time.elapsed();
let execution_time_ms = execution_time.as_millis() as u64;
self.stats.total_execution_time_ms += execution_time_ms;
self.stats.average_execution_time_ms =
self.stats.total_execution_time_ms as f64 / self.stats.total_executions as f64;
if error.is_none() {
if let Some((mutated_names, assigned)) = runmat_vm::take_updated_workspace_state() {
if debug_trace {
debug!(
?mutated_names,
?assigned,
"[repl] mutated names and assigned return values"
);
}
self.workspace_bindings.clear();
for (name, slot) in &mutated_names {
self.bind_workspace_slot(name.clone(), *slot);
}
let previous_workspace = self.workspace_values.clone();
let current_names: HashSet<String> = assigned
.iter()
.filter(|name| {
mutated_names
.get(*name)
.map(|var_id| *var_id < self.variable_array.len())
.unwrap_or(false)
})
.cloned()
.collect();
let removed_names: HashSet<String> = previous_workspace
.keys()
.filter(|name| !current_names.contains(*name))
.cloned()
.collect();
let mut rebuilt_workspace = HashMap::new();
let mut changed_names: HashSet<String> = assigned
.difference(&prev_assigned_snapshot)
.cloned()
.collect();
for name in ¤t_names {
let Some(var_id) = mutated_names.get(name).copied() else {
continue;
};
if var_id >= self.variable_array.len() {
continue;
}
let value_clone = self.variable_array[var_id].clone();
if previous_workspace.get(name) != Some(&value_clone) {
changed_names.insert(name.clone());
}
rebuilt_workspace.insert(name.clone(), value_clone);
}
if debug_trace {
debug!(?changed_names, ?removed_names, "[repl] workspace changes");
}
self.workspace_values = rebuilt_workspace;
if !removed_names.is_empty() {
workspace_snapshot_force_full = true;
} else {
for name in changed_names {
if let Some(value_clone) = self.workspace_values.get(&name).cloned() {
workspace_updates.push(workspace_entry(&name, &value_clone));
if debug_trace {
debug!(name, ?value_clone, "[repl] workspace update");
}
}
}
}
} else {
let previous_workspace = self.workspace_values.clone();
let mut rebuilt_workspace = HashMap::new();
let mut changed_names: HashSet<String> = HashSet::new();
for (name, var_id) in &execution_vars {
if *var_id >= self.variable_array.len() {
continue;
}
let value_clone = self.variable_array[*var_id].clone();
if previous_workspace.get(name) != Some(&value_clone) {
changed_names.insert(name.clone());
}
self.bind_workspace_slot(name.clone(), *var_id);
rebuilt_workspace.insert(name.clone(), value_clone);
}
let removed_names: HashSet<String> = previous_workspace
.keys()
.filter(|name| !rebuilt_workspace.contains_key(*name))
.cloned()
.collect();
self.workspace_values = rebuilt_workspace;
if !removed_names.is_empty() {
workspace_snapshot_force_full = true;
} else {
for name in changed_names {
if let Some(value_clone) = self.workspace_values.get(&name).cloned() {
workspace_updates.push(workspace_entry(&name, &value_clone));
}
}
}
}
self.function_registry = function_registry_after_success;
self.next_semantic_function_id = next_semantic_function_id_after_success;
if let Some((var_id, value)) = ans_update {
self.bind_workspace_slot("ans".to_string(), var_id);
self.workspace_values.insert("ans".to_string(), value);
if debug_trace {
println!("Updated 'ans' to var_id {}", var_id);
}
}
}
if self.verbose {
debug!("Execution completed in {execution_time_ms}ms (JIT: {used_jit})");
}
if !is_expression_stmt
&& !is_semicolon_suppressed
&& last_assign_var.is_some()
&& !display_context.single_stmt_non_assign
&& !display_var_ids.is_empty()
{
if let Some(var_id) = last_store_var_index(&bytecode) {
if var_id < self.variable_array.len() {
result_value = Some(self.variable_array[var_id].clone());
}
} else if matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
&& result_value.is_none()
{
if let Some(v) = self
.variable_array
.iter()
.rev()
.find(|v| !matches!(v, Value::Num(0.0)))
.cloned()
{
result_value = Some(v);
}
}
}
if !is_semicolon_suppressed
&& (!display_var_ids.is_empty()
|| matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
|| display_context.single_assign_var.is_some()
|| (is_expression_stmt
&& matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)))
&& runmat_runtime::console::take_last_value_output().is_none()
{
if display_var_ids.is_empty() {
if let Some(value) = result_value.as_ref() {
let label = last_emit_var_index(&bytecode)
.and_then(|var_id| id_to_name.get(&var_id).cloned())
.or_else(|| {
determine_display_label_from_context(
display_context.single_assign_var,
&id_to_name,
is_expression_stmt,
display_context.single_stmt_non_assign,
)
});
runmat_runtime::console::record_value_output(label.as_deref(), value);
}
} else {
for var_id in display_var_ids {
if let (Some(label), Some(display_value)) =
(id_to_name.get(&var_id), self.variable_array.get(var_id))
{
runmat_runtime::console::record_value_output(
Some(label.as_str()),
display_value,
);
}
}
}
}
let type_info = suppressed_value.as_ref().map(format_type_info);
let streams = runmat_runtime::console::take_thread_buffer()
.into_iter()
.map(|entry| ExecutionStreamEntry {
stream: match entry.stream {
runmat_runtime::console::ConsoleStream::Stdout => ExecutionStreamKind::Stdout,
runmat_runtime::console::ConsoleStream::Stderr => ExecutionStreamKind::Stderr,
runmat_runtime::console::ConsoleStream::ClearScreen => {
ExecutionStreamKind::ClearScreen
}
},
text: entry.text,
timestamp_ms: entry.timestamp_ms,
})
.collect();
let (workspace_entries, snapshot_full) = if workspace_snapshot_force_full {
let mut entries: Vec<WorkspaceEntry> = self
.workspace_values
.iter()
.map(|(name, value)| workspace_entry(name, value))
.collect();
entries.sort_by(|a, b| a.name.cmp(&b.name));
(entries, true)
} else if workspace_updates.is_empty() {
if self.workspace_values.is_empty() {
(workspace_updates, false)
} else {
let mut entries: Vec<WorkspaceEntry> = self
.workspace_values
.iter()
.map(|(name, value)| workspace_entry(name, value))
.collect();
entries.sort_by(|a, b| a.name.cmp(&b.name));
(entries, true)
}
} else {
(workspace_updates, false)
};
let workspace_snapshot = self.build_workspace_snapshot(workspace_entries, snapshot_full);
let figures_touched = runmat_runtime::plotting_hooks::take_recent_figures();
let stdin_events = stdin_events
.lock()
.map(|guard| guard.clone())
.unwrap_or_default();
let warnings = runmat_runtime::warning_store::take_all();
if let Some(runtime_error) = &mut error {
self.normalize_error_namespace(runtime_error);
self.populate_callstack(runtime_error);
}
let suppress_public_value =
is_expression_stmt && matches!(final_stmt_emit, FinalStmtEmitDisposition::Suppressed);
let public_value = if is_semicolon_suppressed || suppress_public_value {
None
} else {
result_value
};
let mut diagnostics = Vec::new();
if let Some(error) = &error {
diagnostics.push(crate::abi::RuntimeDiagnostic {
code: error
.identifier()
.unwrap_or("RunMat:RuntimeError")
.to_string(),
severity: crate::abi::DiagnosticSeverity::Error,
message: error.message().to_string(),
span: None,
});
}
diagnostics.extend(
warnings
.iter()
.map(|warning| crate::abi::RuntimeDiagnostic {
code: warning.identifier.clone(),
severity: crate::abi::DiagnosticSeverity::Warning,
message: warning.message.clone(),
span: None,
}),
);
let display_events = public_value
.as_ref()
.map(|value| crate::abi::DisplayEvent {
label: crate::abi::DisplayLabel::Anonymous,
value: value.clone(),
span: runmat_hir::Span::default(),
})
.into_iter()
.collect();
let profiling = gather_profiling(execution_time_ms);
let outcome = crate::abi::ExecutionOutcome {
flow: public_value
.clone()
.map(crate::abi::RuntimeFlow::Single)
.unwrap_or(crate::abi::RuntimeFlow::NoValue),
workspace_delta: crate::abi::WorkspaceDelta {
version: workspace_snapshot.version,
full_snapshot_required: workspace_snapshot.full,
..crate::abi::WorkspaceDelta::default()
},
display_events,
streams,
diagnostics,
effects: Vec::new(),
suspension: None,
execution_time_ms,
used_jit,
type_info,
figures_touched,
stdin_events,
fusion_plan: fusion_snapshot,
profiling,
};
self.format_mode = runmat_builtins::get_display_format();
Ok(SessionExecution {
outcome,
workspace_snapshot,
})
}
async fn interpret_with_context(
&mut self,
bytecode: &runmat_vm::Bytecode,
) -> Result<runmat_vm::InterpreterOutcome, RuntimeError> {
let source_name = self.current_source_name().to_string();
runmat_vm::interpret_with_vars(
bytecode,
&mut self.variable_array,
Some(source_name.as_str()),
)
.await
}
fn abi_workspace_upserts(
&self,
workspace_names: Vec<String>,
) -> Vec<crate::abi::WorkspaceBindingValue> {
let mut workspace_names = workspace_names;
workspace_names.sort();
workspace_names.dedup();
workspace_names
.into_iter()
.filter_map(|name| {
let value = self.workspace_values.get(&name)?.clone();
let binding = runmat_hir::BindingName(name);
let key = self
.workspace_bindings
.get(&binding.0)
.map(|binding| binding.key.clone())
.unwrap_or_else(|| self.workspace_binding_key(&binding.0));
Some(crate::abi::WorkspaceBindingValue { key, value })
})
.collect()
}
fn abi_workspace_removals(
&self,
previous_workspace_names: HashSet<String>,
) -> Vec<crate::abi::WorkspaceBindingKey> {
let mut removed_names = previous_workspace_names
.into_iter()
.filter(|name| !self.workspace_values.contains_key(name))
.collect::<Vec<_>>();
removed_names.sort();
removed_names
.into_iter()
.map(|name| self.workspace_binding_key(&name))
.collect()
}
}
fn apply_requested_output_policy(
mut outcome: crate::abi::ExecutionOutcome,
requested_outputs: &runmat_hir::RequestedOutputCount,
) -> crate::abi::ExecutionOutcome {
use crate::abi::RuntimeFlow;
use runmat_hir::RequestedOutputCount;
outcome.flow = match requested_outputs {
RequestedOutputCount::Zero => RuntimeFlow::NoValue,
RequestedOutputCount::One => match outcome.flow {
RuntimeFlow::OutputList(mut values) | RuntimeFlow::CommaList(mut values) => {
if values.is_empty() {
RuntimeFlow::NoValue
} else {
RuntimeFlow::Single(values.remove(0))
}
}
flow => flow,
},
RequestedOutputCount::Exactly(count) => {
if *count == 0 {
RuntimeFlow::NoValue
} else if *count == 1 {
match outcome.flow {
RuntimeFlow::OutputList(mut values) | RuntimeFlow::CommaList(mut values) => {
if values.is_empty() {
RuntimeFlow::NoValue
} else {
RuntimeFlow::Single(values.remove(0))
}
}
flow => flow,
}
} else {
match outcome.flow {
RuntimeFlow::NoValue => RuntimeFlow::OutputList(Vec::new()),
RuntimeFlow::Single(value) => RuntimeFlow::OutputList(vec![value]),
RuntimeFlow::OutputList(mut values) | RuntimeFlow::CommaList(mut values) => {
values.truncate(*count);
RuntimeFlow::OutputList(values)
}
RuntimeFlow::DynamicList(handle) => RuntimeFlow::DynamicList(handle),
}
}
}
RequestedOutputCount::CurrentFunctionNargout => outcome.flow,
};
outcome
}
fn resolve_source_identity(
source: &crate::abi::SourceInput,
source_text: &str,
) -> Option<crate::abi::SourceIdentity> {
match source {
crate::abi::SourceInput::Path(path) => {
Some(crate::abi::SourceIdentity::PathAndContentHash {
path: path.clone(),
hash: source_text_hash(source_text),
})
}
crate::abi::SourceInput::Text { name, .. } => {
if name.starts_with('<') {
None
} else {
Some(crate::abi::SourceIdentity::PathAndContentHash {
path: name.clone(),
hash: source_text_hash(source_text),
})
}
}
}
}
fn source_text_hash(source_text: &str) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
source_text.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
fn compile_eval_hook_bytecode(
lowering: &runmat_hir::LoweringResult,
) -> Result<runmat_vm::Bytecode, runmat_vm::CompileError> {
let entrypoint = lowering.assembly.entrypoints.first().ok_or_else(|| {
runmat_vm::CompileError::new("semantic eval hook compile requires an entrypoint")
})?;
let mir = runmat_mir::lowering::lower_assembly(&lowering.assembly)
.map_err(runmat_vm::CompileError::from)?;
let _analysis = runmat_mir::analysis::analyze_assembly(&mir);
runmat_vm::compile(&lowering.assembly, &mir, entrypoint.id)
}
fn execution_workspace_mapping(bytecode: &runmat_vm::Bytecode) -> HashMap<String, usize> {
let Some(layout) = &bytecode.layout else {
return HashMap::new();
};
let mut mapping = HashMap::new();
for entrypoint in layout.entrypoints.values() {
for export in &entrypoint.exports {
mapping.insert(export.name.clone(), export.slot.0);
}
}
mapping
}
fn entry_function(assembly: &runmat_hir::HirAssembly) -> Option<&runmat_hir::HirFunction> {
let entrypoint = assembly.entrypoints.first()?;
assembly
.functions
.iter()
.find(|function| function.id == entrypoint.target)
}
fn entry_statement_count(assembly: &runmat_hir::HirAssembly) -> usize {
entry_function(assembly)
.map(|function| function.body.statements.len())
.unwrap_or(0)
}
fn first_entry_statement(assembly: &runmat_hir::HirAssembly) -> Option<&runmat_hir::HirStmt> {
entry_function(assembly)?.body.statements.first()
}
struct SessionExecution {
outcome: crate::abi::ExecutionOutcome,
workspace_snapshot: WorkspaceSnapshot,
}
async fn source_input_text(
source: crate::abi::SourceInput,
) -> std::result::Result<(String, String), RunError> {
match source {
crate::abi::SourceInput::Text { name, text } => Ok((name, text)),
crate::abi::SourceInput::Path(path) => {
let source_path = resolve_path_source_input(&path).await?;
let text = runmat_filesystem::read_to_string_async(&source_path)
.await
.map_err(|err| {
RunError::Runtime(
build_runtime_error(format!(
"failed to read source path '{}': {err}",
source_path.display()
))
.with_identifier("RunMat:SourceReadFailed")
.build(),
)
})?;
Ok((source_path.to_string_lossy().to_string(), text))
}
}
}
async fn resolve_path_source_input(
path: &str,
) -> std::result::Result<std::path::PathBuf, RunError> {
#[cfg(not(target_arch = "wasm32"))]
{
use runmat_config::project::resolve_project_source_input_from;
use std::path::Path;
let cwd = runmat_filesystem::current_dir().map_err(|err| {
RunError::Runtime(
build_runtime_error(format!(
"failed to resolve current working directory while resolving source path '{path}': {err}"
))
.with_identifier("RunMat:SourceResolveFailed")
.build(),
)
})?;
let resolved = resolve_project_source_input_from(&cwd, Path::new(path)).map_err(|err| {
RunError::Runtime(
build_runtime_error(format!(
"failed to resolve source input '{}' from working directory {}: {}",
path,
cwd.display(),
err
))
.with_identifier("RunMat:EntrypointResolveFailed")
.build(),
)
})?;
Ok(if resolved.is_absolute() {
resolved
} else {
cwd.join(resolved)
})
}
#[cfg(target_arch = "wasm32")]
{
use std::path::PathBuf;
let cwd = runmat_filesystem::current_dir().map_err(|err| {
RunError::Runtime(
build_runtime_error(format!(
"failed to resolve current working directory while resolving source path '{path}': {err}"
))
.with_identifier("RunMat:SourceResolveFailed")
.build(),
)
})?;
let source_path = PathBuf::from(path);
let candidate = if source_path.is_absolute() {
source_path.clone()
} else {
cwd.join(&source_path)
};
if let Ok(metadata) = runmat_filesystem::metadata_async(&candidate).await {
if metadata.is_file() {
return Ok(candidate);
}
}
if source_path.extension().is_none() {
let with_ext = candidate.with_extension("m");
if let Ok(metadata) = runmat_filesystem::metadata_async(&with_ext).await {
if metadata.is_file() {
return Ok(with_ext);
}
}
}
Ok(candidate)
}
}
#[cfg(test)]
mod tests {
#[cfg(not(target_arch = "wasm32"))]
use super::discover_known_project_symbols;
#[cfg(not(target_arch = "wasm32"))]
use super::source_input_text;
#[cfg(not(target_arch = "wasm32"))]
use crate::abi::SourceInput;
#[cfg(not(target_arch = "wasm32"))]
use crate::RunError;
#[cfg(not(target_arch = "wasm32"))]
use std::fs;
#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::Mutex;
#[cfg(not(target_arch = "wasm32"))]
static CWD_LOCK: Mutex<()> = Mutex::new(());
#[cfg(not(target_arch = "wasm32"))]
struct CwdGuard {
original: PathBuf,
}
#[cfg(not(target_arch = "wasm32"))]
impl Drop for CwdGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original);
}
}
#[cfg(not(target_arch = "wasm32"))]
fn push_cwd(path: &Path) -> CwdGuard {
let original = std::env::current_dir().expect("read cwd");
std::env::set_current_dir(path).expect("set cwd");
CwdGuard { original }
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn source_input_path_resolves_named_manifest_entrypoint() {
let _guard = CWD_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
let tmp = tempfile::TempDir::new().unwrap();
fs::create_dir_all(tmp.path().join("src")).unwrap();
fs::write(tmp.path().join("src/main.m"), "x = 1;").unwrap();
fs::write(
tmp.path().join("runmat.toml"),
r#"
[package]
name = "demo"
[sources]
roots = ["src"]
[entrypoints.main]
path = "src/main"
"#,
)
.unwrap();
let _cwd = push_cwd(tmp.path());
let (source_name, source_text) =
futures::executor::block_on(source_input_text(SourceInput::Path("main".to_string())))
.expect("named entrypoint should resolve");
let resolved = std::path::PathBuf::from(source_name)
.canonicalize()
.unwrap();
let expected = tmp.path().join("src/main.m").canonicalize().unwrap();
assert_eq!(
resolved, expected,
"resolved source path should match manifest entrypoint target"
);
assert_eq!(source_text, "x = 1;");
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn source_input_path_infers_m_extension_for_relative_path() {
let _guard = CWD_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
let tmp = tempfile::TempDir::new().unwrap();
fs::create_dir_all(tmp.path().join("src")).unwrap();
fs::write(tmp.path().join("src/main.m"), "x = 1;").unwrap();
let _cwd = push_cwd(tmp.path());
let (source_name, source_text) = futures::executor::block_on(source_input_text(
SourceInput::Path("src/main".to_string()),
))
.expect("path without extension should infer .m");
assert!(source_name.ends_with("src/main.m"));
assert_eq!(source_text.trim(), "x = 1;");
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn source_input_path_errors_for_invalid_named_entrypoint_target() {
let _guard = CWD_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
let tmp = tempfile::TempDir::new().unwrap();
fs::create_dir_all(tmp.path().join("src")).unwrap();
fs::write(
tmp.path().join("runmat.toml"),
r#"
[package]
name = "demo"
[sources]
roots = ["src"]
[entrypoints.server]
module = "app.server"
function = "main"
"#,
)
.unwrap();
let _cwd = push_cwd(tmp.path());
let err =
futures::executor::block_on(source_input_text(SourceInput::Path("server".to_string())))
.expect_err("invalid module/function entrypoint should report resolve error");
let RunError::Runtime(runtime_err) = err else {
panic!("expected runtime error");
};
assert_eq!(
runtime_err.identifier.as_deref(),
Some("RunMat:EntrypointResolveFailed")
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn discover_known_project_symbols_reads_manifest_source_context() {
let _guard = CWD_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
let tmp = tempfile::TempDir::new().unwrap();
fs::create_dir_all(tmp.path().join("+stats")).unwrap();
fs::write(
tmp.path().join("runmat.toml"),
r#"
[package]
name = "demo"
[sources]
roots = ["."]
"#,
)
.unwrap();
fs::write(
tmp.path().join("+stats/summarize.m"),
"function y = summarize(x); y = x; end",
)
.unwrap();
fs::write(tmp.path().join("main.m"), "x = 1;").unwrap();
let _cwd = push_cwd(tmp.path());
let symbols = discover_known_project_symbols(Some(
tmp.path().join("main.m").to_string_lossy().as_ref(),
));
assert!(
symbols.contains("stats.summarize"),
"source-context discovery should include project symbols for eval-hook lowering"
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn discover_known_project_symbols_includes_dependency_alias_qualified_names() {
let _guard = CWD_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
let tmp = tempfile::TempDir::new().unwrap();
let dep_root = tmp.path().join("deps/statslib");
fs::create_dir_all(&dep_root).unwrap();
fs::write(
tmp.path().join("runmat.toml"),
r#"
[package]
name = "demo"
[sources]
roots = ["."]
[dependencies]
statsdep = { path = "deps/statslib" }
"#,
)
.unwrap();
fs::write(
dep_root.join("runmat.toml"),
r#"
[package]
name = "statslib"
[sources]
roots = ["."]
"#,
)
.unwrap();
fs::write(
dep_root.join("summarize.m"),
"function y = summarize(x); y = x; end",
)
.unwrap();
fs::write(tmp.path().join("main.m"), "x = 1;").unwrap();
let _cwd = push_cwd(tmp.path());
let symbols = discover_known_project_symbols(Some(
tmp.path().join("main.m").to_string_lossy().as_ref(),
));
assert!(
symbols.contains("summarize"),
"expected base dependency symbol in known-project discovery"
);
assert!(
symbols.contains("statslib.summarize"),
"expected package-qualified dependency symbol in known-project discovery"
);
assert!(
symbols.contains("statsdep.summarize"),
"expected dependency-alias-qualified symbol in known-project discovery"
);
}
}