1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use crate::chunk::{Chunk, ChunkRef, Op};
5use crate::value::{ModuleFunctionRegistry, VmError, VmValue};
6
7use super::{CallFrame, LocalSlot, Vm};
8
9const CANCEL_GRACE_ASYNC_OP: Duration = Duration::from_millis(250);
10
11#[derive(Clone, Copy)]
12enum DeadlineKind {
13 Scope,
14 InterruptHandler,
15}
16
17impl Vm {
18 #[inline]
25 pub(crate) fn scope_interrupts_clean(&self) -> bool {
26 self.cancel_token.is_none()
27 && self.interrupt_signal_token.is_none()
28 && self.pending_interrupt_signal.is_none()
29 && self.interrupt_handler_deadline.is_none()
30 && self.deadlines.is_empty()
31 }
32
33 pub async fn execute(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
42 self.execute_arc(Arc::new(chunk.clone())).await
43 }
44
45 pub async fn execute_arc(&mut self, chunk: ChunkRef) -> Result<VmValue, VmError> {
51 let registry = self.pool_registry.clone();
52 crate::stdlib::pool::with_pool_registry_scope(registry, async {
53 self.execute_scoped(chunk).await
54 })
55 .await
56 }
57
58 async fn execute_scoped(&mut self, chunk: ChunkRef) -> Result<VmValue, VmError> {
59 let _execution_activity = self
60 .wait_for_graph
61 .register_task(self.runtime_context.task_id.clone());
62 let span_id = crate::tracing::span_start(crate::tracing::SpanKind::Pipeline, "main".into());
63 let result = self.run_chunk(chunk).await;
64 let result = match result {
65 Ok(value) => self.run_pipeline_finish_lifecycle(value).await,
66 Err(error) => {
67 crate::orchestration::clear_pipeline_on_finish();
68 Err(error)
69 }
70 };
71 crate::tracing::span_end(span_id);
72 result
73 }
74
75 async fn run_pipeline_finish_lifecycle(&mut self, value: VmValue) -> Result<VmValue, VmError> {
82 use crate::orchestration::{
83 take_pipeline_on_finish, unsettled_state_snapshot_async, HookEvent,
84 };
85 let _tape_phase =
86 crate::testbench::tape::enter_phase(crate::testbench::tape::TapePhase::RuntimeFinalize);
87
88 let on_finish = take_pipeline_on_finish();
89 let unsettled = unsettled_state_snapshot_async().await;
90
91 let pre_payload = serde_json::json!({
92 "event": HookEvent::PreFinish.as_str(),
93 "return_value": crate::llm::vm_value_to_json(&value),
94 "unsettled": unsettled.to_json(),
95 "has_on_finish": on_finish.is_some(),
96 });
97 self.fire_finish_lifecycle_event(HookEvent::PreFinish, &pre_payload)
98 .await?;
99
100 if !unsettled.is_empty() {
101 let payload = serde_json::json!({
102 "event": HookEvent::OnUnsettledDetected.as_str(),
103 "unsettled": unsettled.to_json(),
104 });
105 self.fire_finish_lifecycle_event(HookEvent::OnUnsettledDetected, &payload)
106 .await?;
107 }
108
109 let final_value = if let Some(closure) = on_finish {
110 let harness_value = crate::harness::Harness::real().into_vm_value();
111 self.call_closure_pub(&closure, &[harness_value, value])
112 .await?
113 } else {
114 value
115 };
116
117 let post_payload = serde_json::json!({
118 "event": HookEvent::PostFinish.as_str(),
119 "return_value": crate::llm::vm_value_to_json(&final_value),
120 "unsettled": unsettled.to_json(),
121 });
122 self.fire_finish_lifecycle_event(HookEvent::PostFinish, &post_payload)
123 .await?;
124
125 Ok(final_value)
126 }
127
128 async fn fire_finish_lifecycle_event(
146 &mut self,
147 event: crate::orchestration::HookEvent,
148 payload: &serde_json::Value,
149 ) -> Result<(), VmError> {
150 use crate::orchestration::{HookControl, HookEvent};
151 let invocations = crate::orchestration::matching_vm_lifecycle_hooks(event, payload);
152 if invocations.is_empty() {
153 return Ok(());
154 }
155 let mut current_payload = payload.clone();
156 for invocation in invocations {
157 let arg = crate::stdlib::json_to_vm_value(¤t_payload);
158 let closure = invocation.resolve(self).await?;
159 let raw = self.call_closure_pub(&closure, &[arg]).await?;
160 let (action, effects) = crate::orchestration::collect_hook_effects_and_action(
161 event,
162 raw,
163 crate::value::VmValue::Nil,
164 )?;
165 crate::orchestration::inject_hook_effects_into_current_session(effects)?;
166 let control = crate::orchestration::parse_hook_control_for_finish(event, &action)?;
167 match control {
168 HookControl::Allow => {}
169 HookControl::Block { reason } => {
170 if matches!(event, HookEvent::PreFinish) {
171 return Err(VmError::Runtime(format!(
172 "PreFinish hook returned block, which is not a valid control: {reason}. \
173 To delay pipeline finish until unsettled work clears, use \
174 OnFinish.block_until_settled (std/lifecycle) or return Modify/Allow \
175 from PreFinish."
176 )));
177 }
178 if matches!(event, HookEvent::PostFinish) {
179 continue;
181 }
182 return Err(VmError::Runtime(format!(
184 "{} hook blocked pipeline finish: {reason}",
185 event.as_str()
186 )));
187 }
188 HookControl::Modify { payload: modified } => {
189 current_payload = modified;
190 }
191 HookControl::Decision { .. } => {}
192 }
193 }
194 Ok(())
195 }
196
197 pub(crate) fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
199 let thrown_value = match &error {
200 VmError::Thrown(v) => v.clone(),
201 other => VmValue::String(arcstr::ArcStr::from(other.to_string())),
202 };
203
204 if let Some(handler) = self.exception_handlers.pop() {
205 if let Some(error_type) = handler.error_type.as_deref() {
206 let matches = match &thrown_value {
208 VmValue::EnumVariant(enum_variant) => enum_variant.has_enum_name(error_type),
209 _ => false,
210 };
211 if !matches {
212 return self.handle_error(error);
213 }
214 }
215
216 self.release_sync_guards_after_unwind(handler.frame_depth, handler.env_scope_depth);
217
218 while self.frames.len() > handler.frame_depth {
219 if let Some(frame) = self.frames.pop() {
220 if let Some(ref dir) = frame.saved_source_dir {
221 crate::stdlib::set_thread_source_dir(dir);
222 }
223 self.iterators.truncate(frame.saved_iterator_depth);
224 self.env = frame.saved_env;
225 }
226 }
227 crate::step_runtime::prune_below_frame(self.frames.len());
228
229 while self
231 .deadlines
232 .last()
233 .is_some_and(|d| d.1 > handler.frame_depth)
234 {
235 self.deadlines.pop();
236 }
237
238 self.env.truncate_scopes(handler.env_scope_depth);
239
240 self.stack.truncate(handler.stack_depth);
241 self.stack.push(thrown_value);
242
243 if let Some(frame) = self.frames.last_mut() {
244 frame.ip = handler.catch_ip;
245 }
246
247 Ok(None)
248 } else {
249 Err(error)
250 }
251 }
252
253 pub(crate) async fn run_chunk(&mut self, chunk: ChunkRef) -> Result<VmValue, VmError> {
254 self.run_chunk_ref(chunk, 0, None, None, None, None).await
255 }
256
257 pub(crate) async fn run_chunk_ref(
258 &mut self,
259 chunk: ChunkRef,
260 argc: usize,
261 saved_source_dir: Option<std::path::PathBuf>,
262 module_functions: Option<ModuleFunctionRegistry>,
263 module_state: Option<crate::value::ModuleState>,
264 local_slots: Option<Vec<LocalSlot>>,
265 ) -> Result<VmValue, VmError> {
266 let debugger = self.debugger_attached();
267 let local_slots = local_slots.unwrap_or_else(|| Self::fresh_local_slots(&chunk));
268 let initial_env = if debugger {
269 Some(self.env.clone())
270 } else {
271 None
272 };
273 let initial_local_slots = if debugger {
274 Some(local_slots.clone())
275 } else {
276 None
277 };
278 self.frames.push(CallFrame {
279 chunk,
280 ip: 0,
281 stack_base: self.stack.len(),
282 saved_env: self.env.clone(),
283 initial_env,
284 initial_local_slots,
285 saved_iterator_depth: self.iterators.len(),
286 fn_name: String::new(),
287 argc,
288 saved_source_dir,
289 module_functions,
290 module_state,
291 local_slots,
292 local_scope_base: self.env.scope_depth().saturating_sub(1),
293 local_scope_depth: 0,
294 });
295
296 self.drive_dispatch_loop(0, false).await
297 }
298
299 pub(crate) async fn drive_until_frame_depth(
306 &mut self,
307 target_depth: usize,
308 ) -> Result<VmValue, VmError> {
309 self.drive_dispatch_loop(target_depth, true).await
310 }
311
312 async fn drive_dispatch_loop(
326 &mut self,
327 target_depth: usize,
328 restore_on_final_pop: bool,
329 ) -> Result<VmValue, VmError> {
330 let _task_activity = self
331 .wait_for_graph
332 .register_task(self.runtime_context.task_id.clone());
333 loop {
334 if !self.scope_interrupts_clean() {
341 if let Some(err) = self.pending_scope_interrupt().await {
342 match self.handle_error(err) {
343 Ok(None) => continue,
344 Ok(Some(val)) => return Ok(val),
345 Err(e) => {
346 self.unwind_frames_to_depth(target_depth);
347 return Err(e);
348 }
349 }
350 }
351 }
352
353 let frame = match self.frames.last_mut() {
354 Some(f) => f,
355 None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
356 };
357
358 if frame.ip >= frame.chunk.code.len() {
359 let val = self.stack.pop().unwrap_or(VmValue::Nil);
360 let val = self.run_step_post_hooks_for_current_frame(val).await?;
361 self.release_sync_guards_for_frame(self.frames.len());
362 let popped_frame = self.frames.pop().unwrap();
363 if let Some(ref dir) = popped_frame.saved_source_dir {
364 crate::stdlib::set_thread_source_dir(dir);
365 }
366 let current_depth = self.frames.len();
367 crate::step_runtime::prune_below_frame(current_depth);
368 while self.deadlines.last().is_some_and(|d| d.1 > current_depth) {
373 self.deadlines.pop();
374 }
375
376 let reached_target = current_depth <= target_depth;
377 if reached_target && !restore_on_final_pop {
378 return Ok(val);
381 }
382 self.iterators.truncate(popped_frame.saved_iterator_depth);
383 self.env = popped_frame.saved_env;
384 self.stack.truncate(popped_frame.stack_base);
385 if reached_target {
386 return Ok(val);
387 }
388 self.stack.push(val);
389 continue;
390 }
391
392 let op_byte = frame.chunk.code[frame.ip];
393 frame.ip += 1;
394
395 let op_result: Result<(), VmError> = if self.scope_interrupts_clean() {
401 let op = match Op::from_byte(op_byte) {
402 Some(op) => op,
403 None => return Err(VmError::InvalidInstruction(op_byte)),
404 };
405 if let Some(result) = self.execute_op_sync(op) {
406 result
407 } else {
408 self.execute_op_async(op).await
409 }
410 } else {
411 match self.execute_op_with_scope_interrupts(op_byte).await {
412 Ok(Some(val)) => return Ok(val),
413 Ok(None) => Ok(()),
414 Err(e) => Err(e),
415 }
416 };
417
418 match op_result {
419 Ok(()) => continue,
420 Err(VmError::Return(val)) => {
421 let val = self.run_step_post_hooks_for_current_frame(val).await?;
422 if let Some(popped_frame) = self.frames.pop() {
423 self.release_sync_guards_for_frame(self.frames.len() + 1);
424 if let Some(ref dir) = popped_frame.saved_source_dir {
425 crate::stdlib::set_thread_source_dir(dir);
426 }
427 let current_depth = self.frames.len();
428 self.exception_handlers
429 .retain(|h| h.frame_depth <= current_depth);
430 crate::step_runtime::prune_below_frame(current_depth);
431 while self.deadlines.last().is_some_and(|d| d.1 > current_depth) {
432 self.deadlines.pop();
433 }
434
435 let reached_target = current_depth <= target_depth;
436 if reached_target && !restore_on_final_pop {
437 return Ok(val);
438 }
439 self.iterators.truncate(popped_frame.saved_iterator_depth);
440 self.env = popped_frame.saved_env;
441 self.stack.truncate(popped_frame.stack_base);
442 if reached_target {
443 return Ok(val);
444 }
445 self.stack.push(val);
446 } else {
447 return Ok(val);
448 }
449 }
450 Err(e) => {
451 if self.error_stack_trace.is_empty() {
453 self.error_stack_trace = self.capture_stack_trace();
454 }
455 let e = match self.apply_step_error_boundary(e) {
462 StepBoundaryOutcome::Returned(val) => {
463 self.error_stack_trace.clear();
464 if self.frames.len() <= target_depth {
465 return Ok(val);
466 }
467 self.stack.push(val);
468 continue;
469 }
470 StepBoundaryOutcome::Throw(err) => err,
471 };
472 match self.handle_error(e) {
473 Ok(None) => {
474 self.error_stack_trace.clear();
475 continue;
476 }
477 Ok(Some(val)) => return Ok(val),
478 Err(e) => {
479 self.unwind_frames_to_depth(target_depth);
480 return Err(self.enrich_error_with_line(e));
481 }
482 }
483 }
484 }
485 }
486 }
487
488 fn unwind_frames_to_depth(&mut self, target_depth: usize) {
495 while self.frames.len() > target_depth {
496 let frame_depth = self.frames.len();
497 if let Some(frame) = self.frames.pop() {
498 self.release_sync_guards_for_frame(frame_depth);
499 if let Some(ref dir) = frame.saved_source_dir {
500 crate::stdlib::set_thread_source_dir(dir);
501 }
502 self.iterators.truncate(frame.saved_iterator_depth);
503 self.env = frame.saved_env;
504 self.stack.truncate(frame.stack_base);
505 }
506 }
507 let current_depth = self.frames.len();
508 crate::step_runtime::prune_below_frame(current_depth);
509 while self.deadlines.last().is_some_and(|d| d.1 > current_depth) {
510 self.deadlines.pop();
511 }
512 }
513
514 pub(crate) fn apply_step_error_boundary(&mut self, error: VmError) -> StepBoundaryOutcome {
520 use crate::step_runtime;
521 if !step_runtime::is_step_budget_exhausted(&error) {
522 return StepBoundaryOutcome::Throw(error);
523 }
524 let Some(step_depth) = step_runtime::active_step_frame_depth() else {
525 return StepBoundaryOutcome::Throw(error);
526 };
527 if step_depth != self.frames.len() {
532 return StepBoundaryOutcome::Throw(error);
533 }
534 let boundary = step_runtime::with_active_step(|step| step.definition.boundary())
535 .unwrap_or(step_runtime::StepErrorBoundary::Fail);
536 match boundary {
537 step_runtime::StepErrorBoundary::Continue => {
538 if let Some(popped) = self.frames.pop() {
542 self.release_sync_guards_for_frame(self.frames.len() + 1);
543 if let Some(ref dir) = popped.saved_source_dir {
544 crate::stdlib::set_thread_source_dir(dir);
545 }
546 let current_depth = self.frames.len();
547 self.exception_handlers
548 .retain(|h| h.frame_depth <= current_depth);
549 step_runtime::pop_and_record(
550 current_depth + 1,
551 "skipped",
552 Some(step_runtime_error_message(&error)),
553 );
554 if self.frames.is_empty() {
555 return StepBoundaryOutcome::Returned(VmValue::Nil);
556 }
557 self.iterators.truncate(popped.saved_iterator_depth);
558 self.env = popped.saved_env;
559 self.stack.truncate(popped.stack_base);
560 }
561 StepBoundaryOutcome::Returned(VmValue::Nil)
562 }
563 step_runtime::StepErrorBoundary::Escalate => {
564 let identity = step_runtime::with_active_step(|step| {
565 (
566 step.definition.name.clone(),
567 step.definition.function.clone(),
568 )
569 });
570 step_runtime::pop_and_record(
571 step_depth,
572 "escalated",
573 Some(step_runtime_error_message(&error)),
574 );
575 let (step_name, function) = identity.unzip();
576 StepBoundaryOutcome::Throw(step_runtime::mark_escalated(
577 error,
578 step_name.as_deref(),
579 function.as_deref(),
580 ))
581 }
582 step_runtime::StepErrorBoundary::Fail => {
583 step_runtime::pop_and_record(
584 step_depth,
585 "failed",
586 Some(step_runtime_error_message(&error)),
587 );
588 StepBoundaryOutcome::Throw(error)
589 }
590 }
591 }
592}
593
594fn next_deadline(
595 scope_deadline: Option<Instant>,
596 interrupt_handler_deadline: Option<Instant>,
597) -> (Option<Instant>, Option<DeadlineKind>) {
598 match (scope_deadline, interrupt_handler_deadline) {
599 (Some(scope), Some(interrupt)) if interrupt < scope => {
600 (Some(interrupt), Some(DeadlineKind::InterruptHandler))
601 }
602 (Some(scope), _) => (Some(scope), Some(DeadlineKind::Scope)),
603 (None, Some(interrupt)) => (Some(interrupt), Some(DeadlineKind::InterruptHandler)),
604 (None, None) => (None, None),
605 }
606}
607
608fn step_runtime_error_message(error: &VmError) -> String {
609 match error {
610 VmError::Thrown(VmValue::Dict(dict)) => dict
611 .get("message")
612 .map(|v| v.display())
613 .unwrap_or_else(|| error.to_string()),
614 _ => error.to_string(),
615 }
616}
617
618pub(crate) enum StepBoundaryOutcome {
619 Returned(VmValue),
620 Throw(VmError),
621}
622
623impl crate::vm::Vm {
624 pub(crate) async fn execute_one_cycle(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
625 if let Some(err) = self.pending_scope_interrupt().await {
626 match self.handle_error(err) {
627 Ok(None) => return Ok(None),
628 Ok(Some(val)) => return Ok(Some((val, false))),
629 Err(e) => return Err(e),
630 }
631 }
632
633 let frame = match self.frames.last_mut() {
634 Some(f) => f,
635 None => {
636 let val = self.stack.pop().unwrap_or(VmValue::Nil);
637 return Ok(Some((val, false)));
638 }
639 };
640
641 if frame.ip >= frame.chunk.code.len() {
642 let val = self.stack.pop().unwrap_or(VmValue::Nil);
643 self.release_sync_guards_for_frame(self.frames.len());
644 let popped_frame = self.frames.pop().unwrap();
645 if self.frames.is_empty() {
646 return Ok(Some((val, false)));
647 }
648 self.iterators.truncate(popped_frame.saved_iterator_depth);
649 self.env = popped_frame.saved_env;
650 self.stack.truncate(popped_frame.stack_base);
651 self.stack.push(val);
652 return Ok(None);
653 }
654
655 let op = frame.chunk.code[frame.ip];
656 frame.ip += 1;
657
658 match self.execute_op_with_scope_interrupts(op).await {
659 Ok(Some(val)) => Ok(Some((val, false))),
660 Ok(None) => Ok(None),
661 Err(VmError::Return(val)) => {
662 if let Some(popped_frame) = self.frames.pop() {
663 self.release_sync_guards_for_frame(self.frames.len() + 1);
664 if let Some(ref dir) = popped_frame.saved_source_dir {
665 crate::stdlib::set_thread_source_dir(dir);
666 }
667 let current_depth = self.frames.len();
668 self.exception_handlers
669 .retain(|h| h.frame_depth <= current_depth);
670 if self.frames.is_empty() {
671 return Ok(Some((val, false)));
672 }
673 self.iterators.truncate(popped_frame.saved_iterator_depth);
674 self.env = popped_frame.saved_env;
675 self.stack.truncate(popped_frame.stack_base);
676 self.stack.push(val);
677 Ok(None)
678 } else {
679 Ok(Some((val, false)))
680 }
681 }
682 Err(e) => {
683 if self.error_stack_trace.is_empty() {
684 self.error_stack_trace = self.capture_stack_trace();
685 }
686 match self.handle_error(e) {
687 Ok(None) => {
688 self.error_stack_trace.clear();
689 Ok(None)
690 }
691 Ok(Some(val)) => Ok(Some((val, false))),
692 Err(e) => Err(self.enrich_error_with_line(e)),
693 }
694 }
695 }
696 }
697
698 async fn execute_op_with_scope_interrupts(
699 &mut self,
700 op: u8,
701 ) -> Result<Option<VmValue>, VmError> {
702 enum ScopeInterruptResult {
703 Op(Result<Option<VmValue>, VmError>),
704 Deadline(DeadlineKind),
705 CancelTimedOut,
706 }
707
708 let (deadline, deadline_kind) = next_deadline(
709 self.deadlines.last().map(|(deadline, _)| *deadline),
710 self.interrupt_handler_deadline,
711 );
712 let cancel_token = self.cancel_token.clone();
713
714 if deadline.is_none() && cancel_token.is_none() {
715 return self.execute_op(op).await;
716 }
717
718 let has_deadline = deadline.is_some();
719 let cancel_requested_at_start = cancel_token
720 .as_ref()
721 .is_some_and(|token| token.load(std::sync::atomic::Ordering::SeqCst));
722 let has_cancel = cancel_token.is_some() && !cancel_requested_at_start;
723 let deadline_sleep = async move {
724 if let Some(deadline) = deadline {
725 tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)).await;
726 } else {
727 std::future::pending::<()>().await;
728 }
729 };
730 let cancel_sleep = async move {
731 if let Some(token) = cancel_token {
732 while !token.load(std::sync::atomic::Ordering::SeqCst) {
733 tokio::time::sleep(Duration::from_millis(10)).await;
734 }
735 } else {
736 std::future::pending::<()>().await;
737 }
738 };
739
740 let result = {
741 let op_future = self.execute_op(op);
742 tokio::pin!(op_future);
743 tokio::select! {
744 result = &mut op_future => ScopeInterruptResult::Op(result),
745 _ = deadline_sleep, if has_deadline => {
746 ScopeInterruptResult::Deadline(deadline_kind.unwrap_or(DeadlineKind::Scope))
747 },
748 _ = cancel_sleep, if has_cancel => {
749 let grace = tokio::time::sleep(CANCEL_GRACE_ASYNC_OP);
750 tokio::pin!(grace);
751 tokio::select! {
752 result = &mut op_future => ScopeInterruptResult::Op(result),
753 _ = &mut grace => ScopeInterruptResult::CancelTimedOut,
754 }
755 }
756 }
757 };
758
759 match result {
760 ScopeInterruptResult::Op(result) => result,
761 ScopeInterruptResult::Deadline(DeadlineKind::Scope) => {
762 self.deadlines.pop();
763 self.cancel_spawned_tasks();
764 Err(Self::deadline_exceeded_error())
765 }
766 ScopeInterruptResult::Deadline(DeadlineKind::InterruptHandler) => {
767 Err(Self::interrupt_handler_timeout_error())
768 }
769 ScopeInterruptResult::CancelTimedOut => {
770 self.cancel_spawned_tasks();
771 let signal = self
772 .take_host_interrupt_signal()
773 .unwrap_or_else(|| "SIGINT".to_string());
774 if self.has_interrupt_handler_for(&signal) {
775 self.dispatch_interrupt_handlers(&signal).await?;
776 }
777 Err(Self::cancelled_error())
778 }
779 }
780 }
781
782 pub(crate) fn deadline_exceeded_error() -> VmError {
783 VmError::Thrown(VmValue::String(arcstr::ArcStr::from("Deadline exceeded")))
784 }
785
786 pub(crate) fn cancelled_error() -> VmError {
787 VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
788 "kind:cancelled:VM cancelled by host",
789 )))
790 }
791
792 pub(crate) fn capture_stack_trace(&self) -> Vec<(String, usize, usize, Option<String>)> {
794 self.frames
795 .iter()
796 .map(|f| {
797 let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
798 let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
799 let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
800 (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
801 })
802 .collect()
803 }
804
805 pub(crate) fn enrich_error_with_line(&self, error: VmError) -> VmError {
809 let line = self
811 .error_stack_trace
812 .last()
813 .map(|(_, l, _, _)| *l)
814 .unwrap_or_else(|| self.current_line());
815 if line == 0 {
816 return error;
817 }
818 let suffix = format!(" (line {line})");
819 match error {
820 VmError::Runtime(msg) => VmError::Runtime(format!("{msg}{suffix}")),
821 VmError::TypeError(msg) => VmError::TypeError(format!("{msg}{suffix}")),
822 VmError::DivisionByZero => VmError::Runtime(format!("Division by zero{suffix}")),
823 VmError::UndefinedVariable(name) => {
824 VmError::Runtime(format!("Undefined variable: {name}{suffix}"))
825 }
826 VmError::UndefinedBuiltin(name) => {
827 VmError::Runtime(format!("Undefined builtin: {name}{suffix}"))
828 }
829 VmError::ImmutableAssignment(name) => VmError::Runtime(format!(
830 "Cannot assign to immutable binding: {name}{suffix}"
831 )),
832 VmError::StackOverflow => {
833 VmError::Runtime(format!("Stack overflow: too many nested calls{suffix}"))
834 }
835 other => other,
841 }
842 }
843}