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