Skip to main content

runmat_core/session/
run.rs

1use super::*;
2
3impl RunMatSession {
4    /// Execute MATLAB/Octave code
5    pub async fn execute(&mut self, input: &str) -> std::result::Result<ExecutionResult, RunError> {
6        self.run(input).await
7    }
8
9    /// Parse, lower, compile, and execute input.
10    pub async fn run(&mut self, input: &str) -> std::result::Result<ExecutionResult, RunError> {
11        let _active = ActiveExecutionGuard::new(self).map_err(|err| {
12            RunError::Runtime(
13                build_runtime_error(err.to_string())
14                    .with_identifier("RunMat:ExecutionAlreadyActive")
15                    .build(),
16            )
17        })?;
18        runmat_vm::set_call_stack_limit(self.callstack_limit);
19        runmat_vm::set_error_namespace(&self.error_namespace);
20        runmat_hir::set_error_namespace(&self.error_namespace);
21        let exec_span = info_span!(
22            "runtime.execute",
23            input_len = input.len(),
24            verbose = self.verbose
25        );
26        let _exec_guard = exec_span.enter();
27        runmat_runtime::console::reset_thread_buffer();
28        runmat_runtime::plotting_hooks::reset_recent_figures();
29        runmat_runtime::warning_store::reset();
30        runmat_builtins::set_display_format(self.format_mode);
31        reset_provider_telemetry();
32        self.interrupt_flag.store(false, Ordering::Relaxed);
33        let _interrupt_guard =
34            runmat_runtime::interrupt::replace_interrupt(Some(self.interrupt_flag.clone()));
35        let start_time = Instant::now();
36        self.stats.total_executions += 1;
37        let debug_trace = std::env::var("RUNMAT_DEBUG_REPL").is_ok();
38        let stdin_events: Arc<Mutex<Vec<StdinEvent>>> = Arc::new(Mutex::new(Vec::new()));
39        let host_async_handler = self.async_input_handler.clone();
40        let stdin_events_async = Arc::clone(&stdin_events);
41        let runtime_async_handler: Arc<runmat_runtime::interaction::AsyncInteractionHandler> =
42            Arc::new(
43                move |prompt: runmat_runtime::interaction::InteractionPromptOwned| {
44                    let request_kind = match prompt.kind {
45                        runmat_runtime::interaction::InteractionKind::Line { echo } => {
46                            InputRequestKind::Line { echo }
47                        }
48                        runmat_runtime::interaction::InteractionKind::KeyPress => {
49                            InputRequestKind::KeyPress
50                        }
51                    };
52                    let request = InputRequest {
53                        prompt: prompt.prompt,
54                        kind: request_kind,
55                    };
56                    let (event_kind, echo_flag) = match &request.kind {
57                        InputRequestKind::Line { echo } => (StdinEventKind::Line, *echo),
58                        InputRequestKind::KeyPress => (StdinEventKind::KeyPress, false),
59                    };
60                    let mut event = StdinEvent {
61                        prompt: request.prompt.clone(),
62                        kind: event_kind,
63                        echo: echo_flag,
64                        value: None,
65                        error: None,
66                    };
67
68                    let stdin_events_async = Arc::clone(&stdin_events_async);
69                    let host_async_handler = host_async_handler.clone();
70                    Box::pin(async move {
71                        let resp: Result<InputResponse, String> =
72                            if let Some(handler) = host_async_handler {
73                                handler(request).await
74                            } else {
75                                match &request.kind {
76                                    InputRequestKind::Line { echo } => {
77                                        runmat_runtime::interaction::default_read_line(
78                                            &request.prompt,
79                                            *echo,
80                                        )
81                                        .map(InputResponse::Line)
82                                    }
83                                    InputRequestKind::KeyPress => {
84                                        runmat_runtime::interaction::default_wait_for_key(
85                                            &request.prompt,
86                                        )
87                                        .map(|_| InputResponse::KeyPress)
88                                    }
89                                }
90                            };
91
92                        let resp = resp.inspect_err(|err| {
93                            event.error = Some(err.clone());
94                            if let Ok(mut guard) = stdin_events_async.lock() {
95                                guard.push(event.clone());
96                            }
97                        })?;
98
99                        let interaction_resp = match resp {
100                            InputResponse::Line(value) => {
101                                event.value = Some(value.clone());
102                                if let Ok(mut guard) = stdin_events_async.lock() {
103                                    guard.push(event);
104                                }
105                                runmat_runtime::interaction::InteractionResponse::Line(value)
106                            }
107                            InputResponse::KeyPress => {
108                                if let Ok(mut guard) = stdin_events_async.lock() {
109                                    guard.push(event);
110                                }
111                                runmat_runtime::interaction::InteractionResponse::KeyPress
112                            }
113                        };
114                        Ok(interaction_resp)
115                    })
116                },
117            );
118        let _async_input_guard =
119            runmat_runtime::interaction::replace_async_handler(Some(runtime_async_handler));
120
121        // Install a stateless expression evaluator for `input()` numeric parsing.
122        //
123        // The hook runs the full parse → lower → compile → interpret pipeline so
124        // that users can type arbitrary MATLAB expressions at an input() prompt:
125        // `sqrt(2)`, `pi/2`, `ones(3)`, `[1 2; 3 4]`, etc.
126        //
127        // Stack-overflow hazard: the hook calls runmat_vm::interpret() while
128        // the outer interpret() is already on the call stack. On WASM the JS event
129        // loop drives both as async state-machines and the WASM linear stack is
130        // large, so nesting is safe. On native the default thread stack is too
131        // small for two nested interpret() invocations, so we instead run the inner
132        // interpret() on a dedicated thread that has its own 16 MB stack and block
133        // the calling future synchronously on the result (safe because the native
134        // executor — futures::executor::block_on — is already synchronous).
135        let compat = self.compat_mode;
136        let _eval_hook_guard =
137            runmat_runtime::interaction::replace_eval_hook(Some(std::sync::Arc::new(
138                move |expr: String| -> runmat_runtime::interaction::EvalHookFuture {
139                    // Shared eval logic, used by both the WASM async path and the
140                    // native thread path below.
141                    async fn eval_expr(
142                        expr: String,
143                        compat: runmat_parser::CompatMode,
144                    ) -> Result<Value, RuntimeError> {
145                        let wrapped = format!("__runmat_input_result__ = ({expr});");
146                        let ast = parse_with_options(&wrapped, ParserOptions::new(compat))
147                            .map_err(|e| {
148                                build_runtime_error(format!("input: parse error: {e}"))
149                                    .with_identifier("RunMat:input:ParseError")
150                                    .build()
151                            })?;
152                        let lowering = runmat_hir::lower(
153                            &ast,
154                            &LoweringContext::new(&HashMap::new(), &HashMap::new()),
155                        )
156                        .map_err(|e| {
157                            build_runtime_error(format!("input: lowering error: {e}"))
158                                .with_identifier("RunMat:input:LowerError")
159                                .build()
160                        })?;
161                        let result_idx = lowering.variables.get("__runmat_input_result__").copied();
162                        let bc = runmat_vm::compile(&lowering.hir, &HashMap::new())
163                            .map_err(RuntimeError::from)?;
164                        let vars = runmat_vm::interpret(&bc).await?;
165                        result_idx
166                            .and_then(|idx| vars.get(idx).cloned())
167                            .ok_or_else(|| {
168                                build_runtime_error("input: expression produced no value")
169                                    .with_identifier("RunMat:input:NoValue")
170                                    .build()
171                            })
172                    }
173
174                    #[cfg(target_arch = "wasm32")]
175                    {
176                        // On WASM: await the inner interpret() directly. The JS async
177                        // runtime handles both futures as cooperative state-machines and
178                        // the WASM linear stack is large enough for the extra frames.
179                        Box::pin(eval_expr(expr, compat))
180                    }
181
182                    #[cfg(not(target_arch = "wasm32"))]
183                    {
184                        // On native: run interpret() on a dedicated thread so it gets
185                        // its own 16 MB stack, fully isolated from the outer interpret()
186                        // call stack. The result is sent back via a tokio oneshot channel
187                        // and awaited asynchronously so the tokio worker thread is never
188                        // blocked by a synchronous recv().
189                        let (tx, rx) = tokio::sync::oneshot::channel();
190                        let spawn_result = std::thread::Builder::new()
191                            .stack_size(16 * 1024 * 1024)
192                            .spawn(move || {
193                                let result = futures::executor::block_on(eval_expr(expr, compat));
194                                let _ = tx.send(result);
195                            });
196                        Box::pin(async move {
197                            spawn_result.map_err(|err| {
198                                build_runtime_error(format!(
199                                    "input: failed to spawn eval thread: {err}"
200                                ))
201                                .with_identifier("RunMat:input:EvalThreadSpawnFailed")
202                                .build()
203                            })?;
204                            rx.await.unwrap_or_else(|_| {
205                                Err(build_runtime_error("input: eval thread panicked")
206                                    .with_identifier("RunMat:input:EvalThreadPanic")
207                                    .build())
208                            })
209                        })
210                    }
211                },
212            )));
213
214        if self.verbose {
215            debug!("Executing: {}", input.trim());
216        }
217
218        let _source_guard = runmat_runtime::source_context::replace_current_source(Some(input));
219
220        let PreparedExecution {
221            ast,
222            lowering,
223            mut bytecode,
224        } = self.compile_input(input)?;
225        if self.verbose {
226            debug!("AST: {ast:?}");
227        }
228        let (hir, updated_vars, updated_functions, var_names_map) = (
229            lowering.hir,
230            lowering.variables,
231            lowering.functions,
232            lowering.var_names,
233        );
234        let max_var_id = updated_vars.values().copied().max().unwrap_or(0);
235        if debug_trace {
236            debug!(?updated_vars, "[repl] updated_vars");
237        }
238        if debug_trace {
239            debug!(workspace_values_before = ?self.workspace_values, "[repl] workspace snapshot before execution");
240        }
241        let id_to_name: HashMap<usize, String> = var_names_map
242            .iter()
243            .map(|(var_id, name)| (var_id.0, name.clone()))
244            .collect();
245        let mut assigned_this_execution: HashSet<String> = HashSet::new();
246        let mut removed_this_execution: HashSet<String> = HashSet::new();
247        let assigned_snapshot: HashSet<String> = updated_vars
248            .keys()
249            .filter(|name| self.workspace_values.contains_key(name.as_str()))
250            .cloned()
251            .collect();
252        let prev_assigned_snapshot = assigned_snapshot.clone();
253        if debug_trace {
254            debug!(?assigned_snapshot, "[repl] assigned snapshot");
255        }
256        let _pending_workspace_guard =
257            runmat_vm::push_pending_workspace(updated_vars.clone(), assigned_snapshot.clone());
258        if self.verbose {
259            debug!("HIR generated successfully");
260        }
261
262        let (single_assign_var, single_stmt_non_assign) = if hir.body.len() == 1 {
263            match &hir.body[0] {
264                runmat_hir::HirStmt::Assign(var_id, _, _, _) => (Some(var_id.0), false),
265                _ => (None, true),
266            }
267        } else {
268            (None, false)
269        };
270
271        bytecode.var_names = id_to_name.clone();
272        if self.verbose {
273            debug!(
274                "Bytecode compiled: {} instructions",
275                bytecode.instructions.len()
276            );
277        }
278
279        #[cfg(not(target_arch = "wasm32"))]
280        let fusion_snapshot = if self.emit_fusion_plan {
281            build_fusion_snapshot(bytecode.accel_graph.as_ref(), &bytecode.fusion_groups)
282        } else {
283            None
284        };
285        #[cfg(target_arch = "wasm32")]
286        let fusion_snapshot: Option<FusionPlanSnapshot> = None;
287
288        // Prepare variable array with existing values before execution
289        self.prepare_variable_array_for_execution(&bytecode, &updated_vars, debug_trace);
290
291        if self.verbose {
292            debug!(
293                "Variable array after preparation: {:?}",
294                self.variable_array
295            );
296            debug!("Updated variable mapping: {updated_vars:?}");
297            debug!("Bytecode instructions: {:?}", bytecode.instructions);
298        }
299
300        #[cfg(feature = "jit")]
301        let mut used_jit = false;
302        #[cfg(not(feature = "jit"))]
303        let used_jit = false;
304        #[cfg(feature = "jit")]
305        let mut execution_completed = false;
306        #[cfg(not(feature = "jit"))]
307        let execution_completed = false;
308        let mut result_value: Option<Value> = None; // Always start fresh for each execution
309        let mut suppressed_value: Option<Value> = None; // Track value for type info when suppressed
310        let mut error = None;
311        let mut workspace_updates: Vec<WorkspaceEntry> = Vec::new();
312        let mut workspace_snapshot_force_full = false;
313        let mut ans_update: Option<(usize, Value)> = None;
314
315        // Check if this is an expression statement (ends with Pop)
316        let is_expression_stmt = bytecode
317            .instructions
318            .last()
319            .map(|instr| matches!(instr, runmat_vm::Instr::Pop))
320            .unwrap_or(false);
321
322        // Determine whether the final statement ended with a semicolon by inspecting the raw input.
323        let is_semicolon_suppressed = {
324            let toks = tokenize_detailed(input);
325            toks.into_iter()
326                .rev()
327                .map(|t| t.token)
328                .find(|token| {
329                    !matches!(
330                        token,
331                        LexToken::Newline
332                            | LexToken::LineComment
333                            | LexToken::BlockComment
334                            | LexToken::Section
335                    )
336                })
337                .map(|t| matches!(t, LexToken::Semicolon))
338                .unwrap_or(false)
339        };
340        let final_stmt_emit = last_displayable_statement_emit_disposition(&hir.body);
341
342        if self.verbose {
343            debug!("HIR body len: {}", hir.body.len());
344            if !hir.body.is_empty() {
345                debug!("HIR statement: {:?}", &hir.body[0]);
346            }
347            debug!("is_semicolon_suppressed: {is_semicolon_suppressed}");
348        }
349
350        // Use JIT for assignments, interpreter for expressions (to capture results properly)
351        #[cfg(feature = "jit")]
352        {
353            if let Some(ref mut jit_engine) = &mut self.jit_engine {
354                if !is_expression_stmt {
355                    // Ensure variable array is large enough
356                    if self.variable_array.len() < bytecode.var_count {
357                        self.variable_array
358                            .resize(bytecode.var_count, Value::Num(0.0));
359                    }
360
361                    if self.verbose {
362                        debug!(
363                            "JIT path for assignment: variable_array size: {}, bytecode.var_count: {}",
364                            self.variable_array.len(),
365                            bytecode.var_count
366                        );
367                    }
368
369                    // Use JIT for assignments
370                    match jit_engine
371                        .execute_or_compile_with_workspace(&bytecode, &mut self.variable_array)
372                    {
373                        Ok((_, actual_used_jit)) => {
374                            used_jit = actual_used_jit;
375                            execution_completed = true;
376                            if actual_used_jit {
377                                self.stats.jit_compiled += 1;
378                            } else {
379                                self.stats.interpreter_fallback += 1;
380                            }
381                            if let Some(var_id) = single_assign_var {
382                                if var_id < self.variable_array.len() {
383                                    let assignment_value = self.variable_array[var_id].clone();
384                                    if !is_semicolon_suppressed {
385                                        result_value = Some(assignment_value);
386                                        if self.verbose {
387                                            debug!("JIT assignment result: {result_value:?}");
388                                        }
389                                    } else {
390                                        suppressed_value = Some(assignment_value);
391                                        if self.verbose {
392                                            debug!("JIT assignment suppressed due to semicolon, captured for type info");
393                                        }
394                                    }
395                                }
396                            }
397
398                            if self.verbose {
399                                debug!(
400                                    "{} assignment successful, variable_array: {:?}",
401                                    if actual_used_jit {
402                                        "JIT"
403                                    } else {
404                                        "Interpreter"
405                                    },
406                                    self.variable_array
407                                );
408                            }
409                        }
410                        Err(e) => {
411                            if self.verbose {
412                                debug!("JIT execution failed: {e}, using interpreter");
413                            }
414                            // Fall back to interpreter
415                        }
416                    }
417                }
418            }
419        }
420
421        // Use interpreter if JIT failed or is disabled
422        if !execution_completed {
423            if self.verbose {
424                debug!(
425                    "Interpreter path: variable_array size: {}, bytecode.var_count: {}",
426                    self.variable_array.len(),
427                    bytecode.var_count
428                );
429            }
430
431            // For expressions, modify bytecode to store result in a temp variable instead of using stack
432            let mut execution_bytecode = bytecode.clone();
433            if is_expression_stmt
434                && matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)
435                && !execution_bytecode.instructions.is_empty()
436            {
437                execution_bytecode.instructions.pop(); // Remove the Pop instruction
438
439                // Add StoreVar instruction to store the result in a temporary variable
440                let temp_var_id = std::cmp::max(execution_bytecode.var_count, max_var_id + 1);
441                execution_bytecode
442                    .instructions
443                    .push(runmat_vm::Instr::StoreVar(temp_var_id));
444                execution_bytecode.var_count = temp_var_id + 1; // Expand variable count for temp variable
445
446                // Ensure our variable array can hold the temporary variable
447                if self.variable_array.len() <= temp_var_id {
448                    self.variable_array.resize(temp_var_id + 1, Value::Num(0.0));
449                }
450
451                if self.verbose {
452                    debug!(
453                        "Modified expression bytecode, new instructions: {:?}",
454                        execution_bytecode.instructions
455                    );
456                }
457            }
458
459            match self.interpret_with_context(&execution_bytecode).await {
460                Ok(runmat_vm::InterpreterOutcome::Completed(results)) => {
461                    // Only increment interpreter_fallback if JIT wasn't attempted
462                    if !self.has_jit() || is_expression_stmt {
463                        self.stats.interpreter_fallback += 1;
464                    }
465                    if self.verbose {
466                        debug!("Interpreter results: {results:?}");
467                    }
468
469                    // Handle assignment statements (x = 42 should show the assigned value unless suppressed)
470                    if hir.body.len() == 1 {
471                        if let runmat_hir::HirStmt::Assign(var_id, _, _, _) = &hir.body[0] {
472                            // For assignments, capture the assigned value for both display and type info
473                            if var_id.0 < self.variable_array.len() {
474                                let assignment_value = self.variable_array[var_id.0].clone();
475                                if !is_semicolon_suppressed {
476                                    result_value = Some(assignment_value);
477                                    if self.verbose {
478                                        debug!("Interpreter assignment result: {result_value:?}");
479                                    }
480                                } else {
481                                    suppressed_value = Some(assignment_value);
482                                    if self.verbose {
483                                        debug!("Interpreter assignment suppressed due to semicolon, captured for type info");
484                                    }
485                                }
486                            }
487                        } else if !is_expression_stmt
488                            && !results.is_empty()
489                            && !is_semicolon_suppressed
490                            && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
491                        {
492                            result_value = Some(results[0].clone());
493                        }
494                    }
495
496                    // For expressions, get the result from the temporary variable (capture for both display and type info)
497                    if is_expression_stmt
498                        && matches!(final_stmt_emit, FinalStmtEmitDisposition::Inline)
499                        && !execution_bytecode.instructions.is_empty()
500                        && result_value.is_none()
501                        && suppressed_value.is_none()
502                    {
503                        let temp_var_id = execution_bytecode.var_count - 1; // The temp variable we added
504                        if temp_var_id < self.variable_array.len() {
505                            let expression_value = self.variable_array[temp_var_id].clone();
506                            if !is_semicolon_suppressed {
507                                // Capture for 'ans' update when output is not suppressed
508                                ans_update = Some((temp_var_id, expression_value.clone()));
509                                result_value = Some(expression_value);
510                                if self.verbose {
511                                    debug!("Expression result from temp var {temp_var_id}: {result_value:?}");
512                                }
513                            } else {
514                                suppressed_value = Some(expression_value);
515                                if self.verbose {
516                                    debug!("Expression suppressed, captured for type info from temp var {temp_var_id}: {suppressed_value:?}");
517                                }
518                            }
519                        }
520                    } else if !is_semicolon_suppressed
521                        && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
522                        && result_value.is_none()
523                    {
524                        result_value = results.into_iter().last();
525                        if self.verbose {
526                            debug!("Fallback result from interpreter: {result_value:?}");
527                        }
528                    }
529
530                    if self.verbose {
531                        debug!("Final result_value: {result_value:?}");
532                    }
533                    debug!("Interpreter execution successful");
534                }
535
536                Err(e) => {
537                    debug!("Interpreter execution failed: {e}");
538                    error = Some(e);
539                }
540            }
541        }
542
543        let last_assign_var = last_unsuppressed_assign_var(&hir.body);
544        let last_expr_emits = last_expr_emits_value(&hir.body);
545        if !is_semicolon_suppressed && result_value.is_none() {
546            if last_assign_var.is_some() || last_expr_emits {
547                if let Some(value) = runmat_runtime::console::take_last_value_output() {
548                    result_value = Some(value);
549                }
550            }
551            if result_value.is_none() {
552                if last_assign_var.is_some() {
553                    if let Some(var_id) = last_emit_var_index(&bytecode) {
554                        if var_id < self.variable_array.len() {
555                            result_value = Some(self.variable_array[var_id].clone());
556                        }
557                    }
558                }
559                if result_value.is_none() {
560                    if let Some(var_id) = last_assign_var {
561                        if var_id < self.variable_array.len() {
562                            result_value = Some(self.variable_array[var_id].clone());
563                        }
564                    }
565                }
566            }
567        }
568
569        let execution_time = start_time.elapsed();
570        let execution_time_ms = execution_time.as_millis() as u64;
571
572        self.stats.total_execution_time_ms += execution_time_ms;
573        self.stats.average_execution_time_ms =
574            self.stats.total_execution_time_ms as f64 / self.stats.total_executions as f64;
575
576        // Update variable names mapping and function definitions if execution was successful
577        if error.is_none() {
578            if let Some((mutated_names, assigned)) = runmat_vm::take_updated_workspace_state() {
579                if let Some(assigned_report) = runmat_vm::take_updated_workspace_assigned_report() {
580                    assigned_this_execution.extend(
581                        assigned_report
582                            .ids
583                            .iter()
584                            .filter_map(|var_id| id_to_name.get(var_id).cloned()),
585                    );
586                    assigned_this_execution.extend(assigned_report.names);
587                    removed_this_execution.extend(
588                        assigned_report
589                            .removed_ids
590                            .iter()
591                            .filter_map(|var_id| id_to_name.get(var_id).cloned()),
592                    );
593                    removed_this_execution.extend(assigned_report.removed_names);
594                }
595                if debug_trace {
596                    debug!(
597                        ?mutated_names,
598                        ?assigned,
599                        ?assigned_this_execution,
600                        "[repl] mutated names and assigned return values"
601                    );
602                }
603                self.variable_names = mutated_names.clone();
604                let previous_workspace = self.workspace_values.clone();
605                let current_names: HashSet<String> = assigned
606                    .iter()
607                    .filter(|name| {
608                        mutated_names
609                            .get(*name)
610                            .map(|var_id| *var_id < self.variable_array.len())
611                            .unwrap_or(false)
612                    })
613                    .cloned()
614                    .collect();
615                let mut removed_names: HashSet<String> = previous_workspace
616                    .keys()
617                    .filter(|name| !current_names.contains(*name))
618                    .cloned()
619                    .collect();
620                removed_names.extend(
621                    removed_this_execution
622                        .into_iter()
623                        .filter(|name| !current_names.contains(name)),
624                );
625                let mut rebuilt_workspace = HashMap::new();
626                let mut changed_names: HashSet<String> = assigned
627                    .difference(&prev_assigned_snapshot)
628                    .cloned()
629                    .collect();
630                changed_names.extend(assigned_this_execution.iter().cloned());
631
632                for name in &current_names {
633                    let Some(var_id) = mutated_names.get(name).copied() else {
634                        continue;
635                    };
636                    if var_id >= self.variable_array.len() {
637                        continue;
638                    }
639                    let value_clone = self.variable_array[var_id].clone();
640                    if previous_workspace.get(name) != Some(&value_clone) {
641                        changed_names.insert(name.clone());
642                    }
643                    rebuilt_workspace.insert(name.clone(), value_clone);
644                }
645
646                if debug_trace {
647                    debug!(?changed_names, ?removed_names, "[repl] workspace changes");
648                }
649
650                self.workspace_values = rebuilt_workspace;
651                if !removed_names.is_empty() {
652                    workspace_snapshot_force_full = true;
653                } else {
654                    for name in changed_names {
655                        if let Some(value_clone) = self.workspace_values.get(&name).cloned() {
656                            workspace_updates.push(workspace_entry(&name, &value_clone));
657                            if debug_trace {
658                                debug!(name, ?value_clone, "[repl] workspace update");
659                            }
660                        }
661                    }
662                }
663            } else {
664                for name in &assigned_this_execution {
665                    if let Some(var_id) =
666                        id_to_name
667                            .iter()
668                            .find_map(|(vid, n)| if n == name { Some(*vid) } else { None })
669                    {
670                        if var_id < self.variable_array.len() {
671                            let value_clone = self.variable_array[var_id].clone();
672                            self.workspace_values
673                                .insert(name.clone(), value_clone.clone());
674                            workspace_updates.push(workspace_entry(name, &value_clone));
675                        }
676                    }
677                }
678            }
679            let mut repl_source_id: Option<SourceId> = None;
680            for (name, stmt) in &updated_functions {
681                if matches!(stmt, runmat_hir::HirStmt::Function { .. }) {
682                    let source_id = *repl_source_id
683                        .get_or_insert_with(|| self.source_pool.intern("<repl>", input));
684                    self.function_source_ids.insert(name.clone(), source_id);
685                }
686            }
687            self.function_definitions = updated_functions;
688            // Apply 'ans' update if applicable (persisting expression result)
689            if let Some((var_id, value)) = ans_update {
690                self.variable_names.insert("ans".to_string(), var_id);
691                self.workspace_values.insert("ans".to_string(), value);
692                if debug_trace {
693                    println!("Updated 'ans' to var_id {}", var_id);
694                }
695            }
696        }
697
698        if self.verbose {
699            debug!("Execution completed in {execution_time_ms}ms (JIT: {used_jit})");
700        }
701
702        if !is_expression_stmt
703            && !is_semicolon_suppressed
704            && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
705            && result_value.is_none()
706        {
707            if let Some(v) = self
708                .variable_array
709                .iter()
710                .rev()
711                .find(|v| !matches!(v, Value::Num(0.0)))
712                .cloned()
713            {
714                result_value = Some(v);
715            }
716        }
717
718        if !is_semicolon_suppressed
719            && matches!(final_stmt_emit, FinalStmtEmitDisposition::NeedsFallback)
720        {
721            if let Some(value) = result_value.as_ref() {
722                let label = determine_display_label_from_context(
723                    single_assign_var,
724                    &id_to_name,
725                    is_expression_stmt,
726                    single_stmt_non_assign,
727                );
728                runmat_runtime::console::record_value_output(label.as_deref(), value);
729            }
730        }
731
732        // Generate type info if we have a suppressed value
733        let type_info = suppressed_value.as_ref().map(format_type_info);
734
735        let streams = runmat_runtime::console::take_thread_buffer()
736            .into_iter()
737            .map(|entry| ExecutionStreamEntry {
738                stream: match entry.stream {
739                    runmat_runtime::console::ConsoleStream::Stdout => ExecutionStreamKind::Stdout,
740                    runmat_runtime::console::ConsoleStream::Stderr => ExecutionStreamKind::Stderr,
741                    runmat_runtime::console::ConsoleStream::ClearScreen => {
742                        ExecutionStreamKind::ClearScreen
743                    }
744                },
745                text: entry.text,
746                timestamp_ms: entry.timestamp_ms,
747            })
748            .collect();
749        let (workspace_entries, snapshot_full) = if workspace_snapshot_force_full {
750            let mut entries: Vec<WorkspaceEntry> = self
751                .workspace_values
752                .iter()
753                .map(|(name, value)| workspace_entry(name, value))
754                .collect();
755            entries.sort_by(|a, b| a.name.cmp(&b.name));
756            (entries, true)
757        } else if workspace_updates.is_empty() {
758            let source_map = if self.workspace_values.is_empty() {
759                &self.variables
760            } else {
761                &self.workspace_values
762            };
763            if source_map.is_empty() {
764                (workspace_updates, false)
765            } else {
766                let mut entries: Vec<WorkspaceEntry> = source_map
767                    .iter()
768                    .map(|(name, value)| workspace_entry(name, value))
769                    .collect();
770                entries.sort_by(|a, b| a.name.cmp(&b.name));
771                (entries, true)
772            }
773        } else {
774            (workspace_updates, false)
775        };
776        let workspace_snapshot = self.build_workspace_snapshot(workspace_entries, snapshot_full);
777        let figures_touched = runmat_runtime::plotting_hooks::take_recent_figures();
778        let stdin_events = stdin_events
779            .lock()
780            .map(|guard| guard.clone())
781            .unwrap_or_default();
782
783        let warnings = runmat_runtime::warning_store::take_all();
784
785        if let Some(runtime_error) = &mut error {
786            self.normalize_error_namespace(runtime_error);
787            self.populate_callstack(runtime_error);
788        }
789
790        let suppress_public_value =
791            is_expression_stmt && matches!(final_stmt_emit, FinalStmtEmitDisposition::Suppressed);
792        let public_value = if is_semicolon_suppressed || suppress_public_value {
793            None
794        } else {
795            result_value
796        };
797
798        self.format_mode = runmat_builtins::get_display_format();
799        Ok(ExecutionResult {
800            value: public_value,
801            execution_time_ms,
802            used_jit,
803            error,
804            type_info,
805            streams,
806            workspace: workspace_snapshot,
807            figures_touched,
808            warnings,
809            profiling: gather_profiling(execution_time_ms),
810            fusion_plan: fusion_snapshot,
811            stdin_events,
812        })
813    }
814
815    /// Interpret bytecode with persistent variable context
816    async fn interpret_with_context(
817        &mut self,
818        bytecode: &runmat_vm::Bytecode,
819    ) -> Result<runmat_vm::InterpreterOutcome, RuntimeError> {
820        let source_name = self.current_source_name().to_string();
821        runmat_vm::interpret_with_vars(
822            bytecode,
823            &mut self.variable_array,
824            Some(source_name.as_str()),
825        )
826        .await
827    }
828}