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::state::{ExecutionDeadlineState, ScopeSpan};
8use super::{CallFrame, LocalSlot, Vm};
9
10const CANCEL_GRACE_ASYNC_OP: Duration = Duration::from_millis(250);
11
12pub(super) fn new_execution_deadline_state(
13 deadline: Option<Instant>,
14) -> Arc<ExecutionDeadlineState> {
15 ExecutionDeadlineState::new(Instant::now(), deadline)
16}
17
18#[cfg(test)]
19thread_local! {
20 static SCOPE_INTERRUPT_ASYNC_DISPATCHES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
21}
22
23#[cfg(test)]
24pub(super) fn reset_scope_interrupt_async_dispatches() {
25 SCOPE_INTERRUPT_ASYNC_DISPATCHES.set(0);
26}
27
28#[cfg(test)]
29pub(super) fn scope_interrupt_async_dispatches() -> u64 {
30 SCOPE_INTERRUPT_ASYNC_DISPATCHES.get()
31}
32
33#[derive(Clone, Copy)]
34enum DeadlineKind {
35 Execution,
36 Scope,
37 InterruptHandler,
38}
39
40impl Vm {
41 #[inline]
48 pub(crate) fn scope_interrupts_clean(&self) -> bool {
49 self.cancel_token.is_none()
50 && self.interrupt_signal_token.is_none()
51 && self.pending_interrupt_signal.is_none()
52 && self.interrupt_handler_deadline.is_none()
53 && !self.execution_deadline.is_active()
54 && self.deadlines.is_empty()
55 }
56
57 pub async fn execute(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
66 self.execute_arc(Arc::new(chunk.clone())).await
67 }
68
69 pub async fn execute_with_timeout(
84 &mut self,
85 chunk: &Chunk,
86 timeout: Duration,
87 ) -> Result<VmValue, VmError> {
88 let deadline = Instant::now().checked_add(timeout).ok_or_else(|| {
89 VmError::Runtime("execution timeout exceeds the platform clock range".to_string())
90 })?;
91 let deadline_guard = self.execution_deadline.install(deadline);
92 let result = self.execute(chunk).await;
93 deadline_guard.complete();
94 result
95 }
96
97 pub async fn execute_arc(&mut self, chunk: ChunkRef) -> Result<VmValue, VmError> {
103 self.ensure_execution_available()?;
104 let registry = self.pool_registry.clone();
105 crate::stdlib::pool::with_pool_registry_scope(registry, async {
106 self.execute_scoped(chunk).await
107 })
108 .await
109 }
110
111 async fn execute_scoped(&mut self, chunk: ChunkRef) -> Result<VmValue, VmError> {
112 let _execution_activity = self
113 .wait_for_graph
114 .register_task(self.runtime_context.task_id.clone());
115 let _span = ScopeSpan::new(crate::tracing::SpanKind::Pipeline, "main".into());
116 let result = self.run_chunk(chunk).await;
117 let result = match result {
118 Ok(value) => self.run_pipeline_finish_lifecycle(value).await,
119 Err(error) => {
120 crate::orchestration::clear_pipeline_on_finish();
121 Err(error)
122 }
123 };
124 result
125 }
126
127 async fn run_pipeline_finish_lifecycle(&mut self, value: VmValue) -> Result<VmValue, VmError> {
134 use crate::orchestration::{
135 take_pipeline_on_finish, unsettled_state_snapshot_async, HookEvent,
136 };
137 let _tape_phase =
138 crate::testbench::tape::enter_phase(crate::testbench::tape::TapePhase::RuntimeFinalize);
139
140 let on_finish = take_pipeline_on_finish();
141 let unsettled = unsettled_state_snapshot_async().await;
142
143 let pre_payload = serde_json::json!({
144 "event": HookEvent::PreFinish.as_str(),
145 "return_value": crate::llm::vm_value_to_json(&value),
146 "unsettled": unsettled.to_json(),
147 "has_on_finish": on_finish.is_some(),
148 });
149 self.fire_finish_lifecycle_event(HookEvent::PreFinish, &pre_payload)
150 .await?;
151
152 if !unsettled.is_empty() {
153 let payload = serde_json::json!({
154 "event": HookEvent::OnUnsettledDetected.as_str(),
155 "unsettled": unsettled.to_json(),
156 });
157 self.fire_finish_lifecycle_event(HookEvent::OnUnsettledDetected, &payload)
158 .await?;
159 }
160
161 let final_value = if let Some(closure) = on_finish {
162 let harness_value = crate::harness::Harness::real().into_vm_value();
163 self.call_closure_pub(&closure, &[harness_value, value])
164 .await?
165 } else {
166 value
167 };
168
169 let post_payload = serde_json::json!({
170 "event": HookEvent::PostFinish.as_str(),
171 "return_value": crate::llm::vm_value_to_json(&final_value),
172 "unsettled": unsettled.to_json(),
173 });
174 self.fire_finish_lifecycle_event(HookEvent::PostFinish, &post_payload)
175 .await?;
176
177 Ok(final_value)
178 }
179
180 async fn fire_finish_lifecycle_event(
198 &mut self,
199 event: crate::orchestration::HookEvent,
200 payload: &serde_json::Value,
201 ) -> Result<(), VmError> {
202 use crate::orchestration::{HookControl, HookEvent};
203 let invocations = crate::orchestration::matching_vm_lifecycle_hooks(event, payload);
204 if invocations.is_empty() {
205 return Ok(());
206 }
207 let mut current_payload = payload.clone();
208 for invocation in invocations {
209 let arg = crate::stdlib::json_to_vm_value(¤t_payload);
210 let closure = invocation.resolve(self).await?;
211 let raw = self.call_closure_pub(&closure, &[arg]).await?;
212 let (action, effects) = crate::orchestration::collect_hook_effects_and_action(
213 event,
214 raw,
215 crate::value::VmValue::Nil,
216 )?;
217 crate::orchestration::inject_hook_effects_into_current_session(effects)?;
218 let control = crate::orchestration::parse_hook_control_for_finish(event, &action)?;
219 match control {
220 HookControl::Allow => {}
221 HookControl::Block { reason } => {
222 if matches!(event, HookEvent::PreFinish) {
223 return Err(VmError::Runtime(format!(
224 "PreFinish hook returned block, which is not a valid control: {reason}. \
225 To delay pipeline finish until unsettled work clears, use \
226 OnFinish.block_until_settled (std/lifecycle) or return Modify/Allow \
227 from PreFinish."
228 )));
229 }
230 if matches!(event, HookEvent::PostFinish) {
231 continue;
233 }
234 return Err(VmError::Runtime(format!(
236 "{} hook blocked pipeline finish: {reason}",
237 event.as_str()
238 )));
239 }
240 HookControl::Modify { payload: modified } => {
241 current_payload = modified;
242 }
243 HookControl::Decision { .. } => {}
244 }
245 }
246 Ok(())
247 }
248
249 pub(crate) fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
251 if matches!(error, VmError::ExecutionDeadlineExceeded) {
252 return Err(error);
253 }
254 let thrown_value = error.thrown_value();
255
256 if let Some(handler) = self.exception_handlers.pop() {
257 if let Some(error_type) = handler.error_type.as_deref() {
258 let matches = match &thrown_value {
260 VmValue::EnumVariant(enum_variant) => enum_variant.has_enum_name(error_type),
261 _ => false,
262 };
263 if !matches {
264 return self.handle_error(error);
265 }
266 }
267
268 self.release_sync_guards_after_unwind(handler.frame_depth, handler.env_scope_depth);
269
270 while self.frames.len() > handler.frame_depth {
271 if let Some(frame) = self.frames.pop() {
272 if let Some(ref dir) = frame.saved_source_dir {
273 crate::stdlib::set_thread_source_dir(dir);
274 }
275 self.iterators.truncate(frame.saved_iterator_depth);
276 self.env = frame.saved_env;
277 }
278 }
279 crate::step_runtime::prune_below_frame(self.frames.len());
280
281 while self
283 .deadlines
284 .last()
285 .is_some_and(|d| d.1 > handler.frame_depth)
286 {
287 self.deadlines.pop();
288 }
289
290 self.env.truncate_scopes(handler.env_scope_depth);
291
292 self.stack.truncate(handler.stack_depth);
293 self.stack.push(thrown_value);
294
295 if let Some(frame) = self.frames.last_mut() {
296 frame.ip = handler.catch_ip;
297 }
298
299 Ok(None)
300 } else {
301 Err(error)
302 }
303 }
304
305 pub(crate) async fn run_chunk(&mut self, chunk: ChunkRef) -> Result<VmValue, VmError> {
306 self.run_chunk_ref(chunk, 0, None, None, None, None).await
307 }
308
309 pub(crate) async fn run_chunk_ref(
310 &mut self,
311 chunk: ChunkRef,
312 argc: usize,
313 saved_source_dir: Option<std::path::PathBuf>,
314 module_functions: Option<ModuleFunctionRegistry>,
315 module_state: Option<crate::value::ModuleState>,
316 local_slots: Option<Vec<LocalSlot>>,
317 ) -> Result<VmValue, VmError> {
318 self.ensure_execution_available()?;
319 let debugger = self.debugger_attached();
320 let local_slots = local_slots.unwrap_or_else(|| Self::fresh_local_slots(&chunk));
321 let initial_env = if debugger {
322 Some(self.env.clone())
323 } else {
324 None
325 };
326 let initial_local_slots = if debugger {
327 Some(local_slots.clone())
328 } else {
329 None
330 };
331 let inline_cache_set = self.inline_cache_set_index_for_chunk(&chunk);
332 self.frames.push(CallFrame {
333 chunk,
334 inline_cache_set,
335 ip: 0,
336 stack_base: self.stack.len(),
337 saved_env: self.env.clone(),
338 initial_env,
339 initial_local_slots,
340 saved_iterator_depth: self.iterators.len(),
341 fn_name: String::new(),
342 argc,
343 saved_source_dir,
344 module_functions,
345 module_state,
346 local_slots,
347 local_scope_base: self.env.scope_depth().saturating_sub(1),
348 local_scope_depth: 0,
349 });
350
351 self.drive_dispatch_loop(0, false).await
352 }
353
354 pub(crate) async fn drive_until_frame_depth(
361 &mut self,
362 target_depth: usize,
363 ) -> Result<VmValue, VmError> {
364 self.drive_dispatch_loop(target_depth, true).await
365 }
366
367 async fn drive_dispatch_loop(
381 &mut self,
382 target_depth: usize,
383 restore_on_final_pop: bool,
384 ) -> Result<VmValue, VmError> {
385 self.ensure_execution_available()?;
386 let _task_activity = self
387 .wait_for_graph
388 .register_task(self.runtime_context.task_id.clone());
389 loop {
390 if !self.scope_interrupts_clean() {
397 if let Some(err) = self.pending_scope_interrupt().await {
398 match self.handle_error(err) {
399 Ok(None) => continue,
400 Ok(Some(val)) => return Ok(val),
401 Err(e) => {
402 self.unwind_frames_to_depth(target_depth);
403 return Err(e);
404 }
405 }
406 }
407 }
408
409 let frame = match self.frames.last_mut() {
410 Some(f) => f,
411 None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
412 };
413
414 if frame.ip >= frame.chunk.code.len() {
415 let val = self.stack.pop().unwrap_or(VmValue::Nil);
416 let val = self.run_step_post_hooks_for_current_frame(val).await?;
417 self.release_sync_guards_for_frame(self.frames.len());
418 let popped_frame = self.frames.pop().unwrap();
419 if let Some(ref dir) = popped_frame.saved_source_dir {
420 crate::stdlib::set_thread_source_dir(dir);
421 }
422 let current_depth = self.frames.len();
423 crate::step_runtime::prune_below_frame(current_depth);
424 while self.deadlines.last().is_some_and(|d| d.1 > current_depth) {
429 self.deadlines.pop();
430 }
431
432 let reached_target = current_depth <= target_depth;
433 if reached_target && !restore_on_final_pop {
434 return Ok(val);
437 }
438 self.iterators.truncate(popped_frame.saved_iterator_depth);
439 self.env = popped_frame.saved_env;
440 self.stack.truncate(popped_frame.stack_base);
441 if reached_target {
442 return Ok(val);
443 }
444 self.stack.push(val);
445 continue;
446 }
447
448 let op_byte = frame.chunk.code[frame.ip];
449 if let Some(coverage) = self.coverage.as_mut() {
455 coverage.record(&frame.chunk, frame.ip);
456 }
457 frame.ip += 1;
458
459 let op = match Op::from_byte(op_byte) {
464 Some(op) => op,
465 None => return Err(VmError::InvalidInstruction(op_byte)),
466 };
467 let op_result: Result<(), VmError> = if let Some(result) = self.execute_op_sync(op) {
468 result
469 } else if self.scope_interrupts_clean() {
470 self.execute_op_async(op).await
471 } else {
472 match self.execute_op_with_scope_interrupts(op_byte).await {
473 Ok(Some(val)) => return Ok(val),
474 Ok(None) => Ok(()),
475 Err(e) => Err(e),
476 }
477 };
478
479 match op_result {
480 Ok(()) => continue,
481 Err(VmError::Return(val)) => {
482 let val = self.run_step_post_hooks_for_current_frame(val).await?;
483 if let Some(popped_frame) = self.frames.pop() {
484 self.release_sync_guards_for_frame(self.frames.len() + 1);
485 if let Some(ref dir) = popped_frame.saved_source_dir {
486 crate::stdlib::set_thread_source_dir(dir);
487 }
488 let current_depth = self.frames.len();
489 self.exception_handlers
490 .retain(|h| h.frame_depth <= current_depth);
491 crate::step_runtime::prune_below_frame(current_depth);
492 while self.deadlines.last().is_some_and(|d| d.1 > current_depth) {
493 self.deadlines.pop();
494 }
495
496 let reached_target = current_depth <= target_depth;
497 if reached_target && !restore_on_final_pop {
498 return Ok(val);
499 }
500 self.iterators.truncate(popped_frame.saved_iterator_depth);
501 self.env = popped_frame.saved_env;
502 self.stack.truncate(popped_frame.stack_base);
503 if reached_target {
504 return Ok(val);
505 }
506 self.stack.push(val);
507 } else {
508 return Ok(val);
509 }
510 }
511 Err(e) => {
512 if self.error_stack_trace.is_empty() {
514 self.error_stack_trace = self.capture_stack_trace();
515 }
516 let e = match self.apply_step_error_boundary(e) {
523 StepBoundaryOutcome::Returned(val) => {
524 self.error_stack_trace.clear();
525 if self.frames.len() <= target_depth {
526 return Ok(val);
527 }
528 self.stack.push(val);
529 continue;
530 }
531 StepBoundaryOutcome::Throw(err) => err,
532 };
533 match self.handle_error(e) {
534 Ok(None) => {
535 self.error_stack_trace.clear();
536 continue;
537 }
538 Ok(Some(val)) => return Ok(val),
539 Err(e) => {
540 self.unwind_frames_to_depth(target_depth);
541 return Err(self.enrich_error_with_line(e));
542 }
543 }
544 }
545 }
546 }
547 }
548
549 fn unwind_frames_to_depth(&mut self, target_depth: usize) {
556 while self.frames.len() > target_depth {
557 let frame_depth = self.frames.len();
558 if let Some(frame) = self.frames.pop() {
559 self.release_sync_guards_for_frame(frame_depth);
560 if let Some(ref dir) = frame.saved_source_dir {
561 crate::stdlib::set_thread_source_dir(dir);
562 }
563 self.iterators.truncate(frame.saved_iterator_depth);
564 self.env = frame.saved_env;
565 self.stack.truncate(frame.stack_base);
566 }
567 }
568 let current_depth = self.frames.len();
569 crate::step_runtime::prune_below_frame(current_depth);
570 while self.deadlines.last().is_some_and(|d| d.1 > current_depth) {
571 self.deadlines.pop();
572 }
573 }
574
575 pub(crate) fn apply_step_error_boundary(&mut self, error: VmError) -> StepBoundaryOutcome {
581 use crate::step_runtime;
582 if !step_runtime::is_step_budget_exhausted(&error) {
583 return StepBoundaryOutcome::Throw(error);
584 }
585 let Some(step_depth) = step_runtime::active_step_frame_depth() else {
586 return StepBoundaryOutcome::Throw(error);
587 };
588 if step_depth != self.frames.len() {
593 return StepBoundaryOutcome::Throw(error);
594 }
595 let boundary = step_runtime::with_active_step(|step| step.definition.boundary())
596 .unwrap_or(step_runtime::StepErrorBoundary::Fail);
597 match boundary {
598 step_runtime::StepErrorBoundary::Continue => {
599 if let Some(popped) = self.frames.pop() {
603 self.release_sync_guards_for_frame(self.frames.len() + 1);
604 if let Some(ref dir) = popped.saved_source_dir {
605 crate::stdlib::set_thread_source_dir(dir);
606 }
607 let current_depth = self.frames.len();
608 self.exception_handlers
609 .retain(|h| h.frame_depth <= current_depth);
610 step_runtime::pop_and_record(
611 current_depth + 1,
612 "skipped",
613 Some(step_runtime_error_message(&error)),
614 );
615 if self.frames.is_empty() {
616 return StepBoundaryOutcome::Returned(VmValue::Nil);
617 }
618 self.iterators.truncate(popped.saved_iterator_depth);
619 self.env = popped.saved_env;
620 self.stack.truncate(popped.stack_base);
621 }
622 StepBoundaryOutcome::Returned(VmValue::Nil)
623 }
624 step_runtime::StepErrorBoundary::Escalate => {
625 let identity = step_runtime::with_active_step(|step| {
626 (
627 step.definition.name.clone(),
628 step.definition.function.clone(),
629 )
630 });
631 step_runtime::pop_and_record(
632 step_depth,
633 "escalated",
634 Some(step_runtime_error_message(&error)),
635 );
636 let (step_name, function) = identity.unzip();
637 StepBoundaryOutcome::Throw(step_runtime::mark_escalated(
638 error,
639 step_name.as_deref(),
640 function.as_deref(),
641 ))
642 }
643 step_runtime::StepErrorBoundary::Fail => {
644 step_runtime::pop_and_record(
645 step_depth,
646 "failed",
647 Some(step_runtime_error_message(&error)),
648 );
649 StepBoundaryOutcome::Throw(error)
650 }
651 }
652 }
653}
654
655fn next_deadline(
656 execution_deadline: Option<Instant>,
657 scope_deadline: Option<Instant>,
658 interrupt_handler_deadline: Option<Instant>,
659) -> (Option<Instant>, Option<DeadlineKind>) {
660 [
661 (execution_deadline, DeadlineKind::Execution),
662 (scope_deadline, DeadlineKind::Scope),
663 (interrupt_handler_deadline, DeadlineKind::InterruptHandler),
664 ]
665 .into_iter()
666 .filter_map(|(deadline, kind)| deadline.map(|deadline| (deadline, kind)))
667 .min_by_key(|(deadline, _)| *deadline)
668 .map_or((None, None), |(deadline, kind)| {
669 (Some(deadline), Some(kind))
670 })
671}
672
673fn step_runtime_error_message(error: &VmError) -> String {
674 match error {
675 VmError::Thrown(VmValue::Dict(dict)) => dict
676 .get("message")
677 .map(|v| v.display())
678 .unwrap_or_else(|| error.to_string()),
679 _ => error.to_string(),
680 }
681}
682
683pub(crate) enum StepBoundaryOutcome {
684 Returned(VmValue),
685 Throw(VmError),
686}
687
688impl crate::vm::Vm {
689 pub(crate) async fn execute_one_cycle(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
690 if let Some(err) = self.pending_scope_interrupt().await {
691 match self.handle_error(err) {
692 Ok(None) => return Ok(None),
693 Ok(Some(val)) => return Ok(Some((val, false))),
694 Err(e) => return Err(e),
695 }
696 }
697
698 let frame = match self.frames.last_mut() {
699 Some(f) => f,
700 None => {
701 let val = self.stack.pop().unwrap_or(VmValue::Nil);
702 return Ok(Some((val, false)));
703 }
704 };
705
706 if frame.ip >= frame.chunk.code.len() {
707 let val = self.stack.pop().unwrap_or(VmValue::Nil);
708 self.release_sync_guards_for_frame(self.frames.len());
709 let popped_frame = self.frames.pop().unwrap();
710 if self.frames.is_empty() {
711 return Ok(Some((val, false)));
712 }
713 self.iterators.truncate(popped_frame.saved_iterator_depth);
714 self.env = popped_frame.saved_env;
715 self.stack.truncate(popped_frame.stack_base);
716 self.stack.push(val);
717 return Ok(None);
718 }
719
720 let op = frame.chunk.code[frame.ip];
721 frame.ip += 1;
722
723 match self.execute_op_with_scope_interrupts(op).await {
724 Ok(Some(val)) => Ok(Some((val, false))),
725 Ok(None) => Ok(None),
726 Err(VmError::Return(val)) => {
727 if let Some(popped_frame) = self.frames.pop() {
728 self.release_sync_guards_for_frame(self.frames.len() + 1);
729 if let Some(ref dir) = popped_frame.saved_source_dir {
730 crate::stdlib::set_thread_source_dir(dir);
731 }
732 let current_depth = self.frames.len();
733 self.exception_handlers
734 .retain(|h| h.frame_depth <= current_depth);
735 if self.frames.is_empty() {
736 return Ok(Some((val, false)));
737 }
738 self.iterators.truncate(popped_frame.saved_iterator_depth);
739 self.env = popped_frame.saved_env;
740 self.stack.truncate(popped_frame.stack_base);
741 self.stack.push(val);
742 Ok(None)
743 } else {
744 Ok(Some((val, false)))
745 }
746 }
747 Err(e) => {
748 if self.error_stack_trace.is_empty() {
749 self.error_stack_trace = self.capture_stack_trace();
750 }
751 match self.handle_error(e) {
752 Ok(None) => {
753 self.error_stack_trace.clear();
754 Ok(None)
755 }
756 Ok(Some(val)) => Ok(Some((val, false))),
757 Err(e) => Err(self.enrich_error_with_line(e)),
758 }
759 }
760 }
761 }
762
763 async fn execute_op_with_scope_interrupts(
764 &mut self,
765 op: u8,
766 ) -> Result<Option<VmValue>, VmError> {
767 #[cfg(test)]
768 SCOPE_INTERRUPT_ASYNC_DISPATCHES
769 .set(SCOPE_INTERRUPT_ASYNC_DISPATCHES.get().saturating_add(1));
770
771 enum ScopeInterruptResult {
772 Op(Result<Option<VmValue>, VmError>),
773 Deadline(DeadlineKind),
774 CancelTimedOut,
775 }
776
777 let (deadline, deadline_kind) = next_deadline(
778 self.execution_deadline.current(),
779 self.deadlines.last().map(|(deadline, _)| *deadline),
780 self.interrupt_handler_deadline,
781 );
782 let cancel_token = self.cancel_token.clone();
783
784 if deadline.is_none() && cancel_token.is_none() {
785 return self.execute_op(op).await;
786 }
787
788 let has_deadline = deadline.is_some();
789 let cancel_requested_at_start = cancel_token
790 .as_ref()
791 .is_some_and(|token| token.load(std::sync::atomic::Ordering::SeqCst));
792 let has_cancel = cancel_token.is_some() && !cancel_requested_at_start;
793 let deadline_sleep = async move {
794 if let Some(deadline) = deadline {
795 tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)).await;
796 } else {
797 std::future::pending::<()>().await;
798 }
799 };
800 let cancel_sleep = async move {
801 if let Some(token) = cancel_token {
802 while !token.load(std::sync::atomic::Ordering::SeqCst) {
803 tokio::time::sleep(Duration::from_millis(10)).await;
804 }
805 } else {
806 std::future::pending::<()>().await;
807 }
808 };
809
810 let result = {
811 let op_future = self.execute_op(op);
812 tokio::pin!(op_future);
813 tokio::select! {
814 result = &mut op_future => ScopeInterruptResult::Op(result),
815 _ = deadline_sleep, if has_deadline => {
816 ScopeInterruptResult::Deadline(deadline_kind.unwrap_or(DeadlineKind::Scope))
817 },
818 _ = cancel_sleep, if has_cancel => {
819 let grace = tokio::time::sleep(CANCEL_GRACE_ASYNC_OP);
820 tokio::pin!(grace);
821 tokio::select! {
822 result = &mut op_future => ScopeInterruptResult::Op(result),
823 _ = &mut grace => ScopeInterruptResult::CancelTimedOut,
824 }
825 }
826 }
827 };
828
829 match result {
830 ScopeInterruptResult::Op(result) => result,
831 ScopeInterruptResult::Deadline(DeadlineKind::Execution) => {
832 self.cancel_spawned_tasks();
833 Err(VmError::ExecutionDeadlineExceeded)
834 }
835 ScopeInterruptResult::Deadline(DeadlineKind::Scope) => {
836 self.deadlines.pop();
837 self.cancel_spawned_tasks();
838 Err(Self::deadline_exceeded_error())
839 }
840 ScopeInterruptResult::Deadline(DeadlineKind::InterruptHandler) => {
841 Err(Self::interrupt_handler_timeout_error())
842 }
843 ScopeInterruptResult::CancelTimedOut => {
844 self.cancel_spawned_tasks();
845 let signal = self
846 .take_host_interrupt_signal()
847 .unwrap_or_else(|| "SIGINT".to_string());
848 if self.has_interrupt_handler_for(&signal) {
849 self.dispatch_interrupt_handlers(&signal).await?;
850 }
851 Err(Self::cancelled_error())
852 }
853 }
854 }
855
856 pub(crate) fn deadline_exceeded_error() -> VmError {
857 VmError::Thrown(VmValue::String(arcstr::ArcStr::from("Deadline exceeded")))
858 }
859
860 pub(crate) fn cancelled_error() -> VmError {
861 VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
862 "kind:cancelled:VM cancelled by host",
863 )))
864 }
865
866 pub(crate) fn capture_stack_trace(&self) -> Vec<(String, usize, usize, Option<String>)> {
868 self.frames
869 .iter()
870 .map(|f| {
871 let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
872 let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
873 let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
874 (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
875 })
876 .collect()
877 }
878
879 pub(crate) fn enrich_error_with_line(&self, error: VmError) -> VmError {
883 let (line, file) = self
888 .error_stack_trace
889 .last()
890 .map(|(_, l, _, f)| (*l, f.clone()))
891 .unwrap_or_else(|| (self.current_line(), None));
892 if line == 0 {
893 return error;
894 }
895 let suffix = match file.as_deref() {
896 Some(path) => {
897 let name = std::path::Path::new(path)
898 .file_name()
899 .and_then(|n| n.to_str())
900 .unwrap_or(path);
901 format!(" ({name}:{line})")
902 }
903 None => format!(" (line {line})"),
904 };
905 match error {
906 VmError::Runtime(msg) => VmError::Runtime(format!("{msg}{suffix}")),
907 VmError::TypeError(msg) => VmError::TypeError(format!("{msg}{suffix}")),
908 VmError::DivisionByZero => VmError::Runtime(format!("Division by zero{suffix}")),
909 VmError::UndefinedVariable(name) => {
910 VmError::Runtime(format!("Undefined variable: {name}{suffix}"))
911 }
912 VmError::UndefinedBuiltin(name) => {
913 VmError::Runtime(format!("Undefined builtin: {name}{suffix}"))
914 }
915 VmError::ImmutableAssignment(name) => VmError::Runtime(format!(
916 "Cannot assign to immutable binding: {name}{suffix}"
917 )),
918 VmError::StackOverflow => {
919 VmError::Runtime(format!("Stack overflow: too many nested calls{suffix}"))
920 }
921 other => other,
927 }
928 }
929}
930
931#[cfg(test)]
932mod tests {
933 use super::*;
934 use crate::compiler::Compiler;
935 use crate::stdlib::register_vm_stdlib;
936 use harn_lexer::Lexer;
937 use harn_parser::Parser;
938 use std::sync::atomic::{AtomicBool, Ordering};
939
940 fn compile_harn(source: &str) -> Chunk {
941 let mut lexer = Lexer::new(source);
942 let tokens = lexer.tokenize().unwrap();
943 let mut parser = Parser::new(tokens);
944 let program = parser.parse().unwrap();
945 Compiler::new().compile(&program).unwrap()
946 }
947
948 #[tokio::test(flavor = "current_thread")]
949 async fn dropping_timed_execution_poison_vm_reuse() {
950 let local = tokio::task::LocalSet::new();
951 local
952 .run_until(async {
953 let slow = compile_harn(
954 r#"pipeline default() {
955 pipeline_on_finish({ _h, value -> value })
956 const child = spawn {
957 child_started()
958 wait_for_child_release()
959 child_effect()
960 }
961 __io_println("first-started")
962 sleep(5s)
963 return 7
964}"#,
965 );
966 let quick = compile_harn("pipeline default() { return 42 }");
967 let mut vm = Vm::new();
968 register_vm_stdlib(&mut vm);
969 let child_started = Arc::new(AtomicBool::new(false));
970 let child_effect = Arc::new(AtomicBool::new(false));
971 let child_release = Arc::new(tokio::sync::Notify::new());
972 let started_for_builtin = Arc::clone(&child_started);
973 vm.register_builtin("child_started", move |_args, _output| {
974 started_for_builtin.store(true, Ordering::Release);
975 Ok(VmValue::Nil)
976 });
977 let effect_for_builtin = Arc::clone(&child_effect);
978 vm.register_builtin("child_effect", move |_args, _output| {
979 effect_for_builtin.store(true, Ordering::Release);
980 Ok(VmValue::Nil)
981 });
982 let release_for_builtin = Arc::clone(&child_release);
983 vm.register_async_builtin("wait_for_child_release", move |_ctx, _args| {
984 let release = Arc::clone(&release_for_builtin);
985 async move {
986 release.notified().await;
987 Ok(VmValue::Nil)
988 }
989 });
990 let callable = vm
991 .load_module_exports_from_source(
992 "<abandoned-execution-test>",
993 "pub fn answer() { return 42 }",
994 )
995 .await
996 .unwrap()
997 .remove("answer")
998 .unwrap();
999
1000 let mut execution =
1001 Box::pin(vm.execute_with_timeout(&slow, Duration::from_secs(30)));
1002 tokio::select! {
1003 biased;
1004 result = &mut execution => panic!("slow execution unexpectedly finished: {result:?}"),
1005 started = tokio::time::timeout(Duration::from_secs(1), async {
1006 while !child_started.load(Ordering::Acquire) {
1007 tokio::task::yield_now().await;
1008 }
1009 }) => started.expect("spawned child did not start"),
1010 }
1011 drop(execution);
1012
1013 assert!(!vm.execution_deadline.is_active());
1014 assert!(vm.output().contains("first-started"));
1015 assert!(!vm.frames.is_empty(), "fixture must abandon a live frame");
1016 assert!(crate::orchestration::take_pipeline_on_finish().is_none());
1017 let frame_depth = vm.frames.len();
1018 let output = vm.output().to_string();
1019 let error = vm.execute(&quick).await.unwrap_err();
1020 assert!(matches!(error, VmError::AbandonedExecution));
1021 let closure_error = vm.call_closure_pub(&callable, &[]).await.unwrap_err();
1022 assert!(matches!(closure_error, VmError::AbandonedExecution));
1023 let source_cache_len = vm.source_cache.len();
1024 let module_cache_len = vm.module_cache.len();
1025 let module_error = vm
1026 .load_module_exports_from_source(
1027 "<poisoned-module-load>",
1028 "pub fn poisoned() { return 0 }",
1029 )
1030 .await
1031 .unwrap_err();
1032 assert!(matches!(module_error, VmError::AbandonedExecution));
1033 assert_eq!(vm.source_cache.len(), source_cache_len);
1034 assert_eq!(vm.module_cache.len(), module_cache_len);
1035 let start_error = vm.start(&quick).unwrap_err();
1036 assert!(matches!(start_error, VmError::AbandonedExecution));
1037 let restart_error = vm.restart_frame(0).unwrap_err();
1038 assert!(matches!(restart_error, VmError::AbandonedExecution));
1039 assert_eq!(vm.frames.len(), frame_depth);
1040 assert_eq!(vm.output(), output);
1041 drop(vm);
1042 child_release.notify_one();
1043 for _ in 0..10 {
1044 tokio::task::yield_now().await;
1045 }
1046 assert!(
1047 !child_effect.load(Ordering::Acquire),
1048 "dropping an abandoned VM must abort spawned side effects"
1049 );
1050 })
1051 .await;
1052 }
1053
1054 #[tokio::test(flavor = "current_thread")]
1055 async fn timed_finite_loop_keeps_sync_opcodes_on_direct_dispatch() {
1056 let local = tokio::task::LocalSet::new();
1057 local
1058 .run_until(async {
1059 let chunk = compile_harn(
1060 r"
1061pipeline default() {
1062 let total = 0
1063 for i in 0 to 10000 {
1064 total = total + i
1065 }
1066 return total
1067}
1068",
1069 );
1070 let mut vm = Vm::new();
1071 register_vm_stdlib(&mut vm);
1072 reset_scope_interrupt_async_dispatches();
1073
1074 let value = vm
1075 .execute_with_timeout(&chunk, Duration::from_secs(1))
1076 .await
1077 .unwrap();
1078
1079 assert!(matches!(value, VmValue::Int(_)));
1080 assert!(
1081 scope_interrupt_async_dispatches() <= 4,
1082 "finite sync loop fell back to per-op async dispatch"
1083 );
1084 })
1085 .await;
1086 }
1087}