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