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