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