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(
81 &mut self,
82 chunk: &Chunk,
83 timeout: Duration,
84 ) -> Result<VmValue, VmError> {
85 let deadline = Instant::now().checked_add(timeout).ok_or_else(|| {
86 VmError::Runtime("execution timeout exceeds the platform clock range".to_string())
87 })?;
88 crate::orchestration::scope_ambient_transaction(async {
89 let pipeline_checkpoint = crate::orchestration::checkpoint_pipeline_lifecycle();
90 let deadline_guard = self.execution_deadline.install(deadline);
91 let result = crate::tracing::checkpoint_future(self.execute(chunk)).await;
92 deadline_guard.complete();
93 pipeline_checkpoint.complete();
94 result
95 })
96 .await
97 }
98
99 pub async fn execute_arc(&mut self, chunk: ChunkRef) -> Result<VmValue, VmError> {
105 self.ensure_execution_available()?;
106 let registry = self.pool_registry.clone();
107 let owner = crate::observability::execution_scope::mint_execution_scope();
108 let ambient = crate::orchestration::AmbientExecutionScope::capture_for_top_level_execution(
109 owner,
110 self.llm_mock_context.clone(),
111 );
112 let execution = crate::stdlib::pool::with_pool_registry_scope(registry, async {
113 self.execute_scoped(chunk).await
114 });
115 crate::orchestration::scope_ambient(ambient, execution).await
116 }
117
118 async fn execute_scoped(&mut self, chunk: ChunkRef) -> Result<VmValue, VmError> {
119 let _execution_activity = self
120 .wait_for_graph
121 .register_task(self.runtime_context.task_id.clone());
122 let _span = ScopeSpan::new(crate::tracing::SpanKind::Pipeline, "main".into());
123 let result = self.run_chunk(chunk).await;
124 let result = match result {
125 Ok(value) => self.run_pipeline_finish_lifecycle(value).await,
126 Err(error) => {
127 crate::orchestration::clear_pipeline_on_finish();
128 Err(error)
129 }
130 };
131 result
132 }
133
134 async fn run_pipeline_finish_lifecycle(&mut self, value: VmValue) -> Result<VmValue, VmError> {
141 use crate::orchestration::{
142 take_pipeline_on_finish, unsettled_state_snapshot_async, HookEvent,
143 };
144 let _tape_phase =
145 crate::testbench::tape::enter_phase(crate::testbench::tape::TapePhase::RuntimeFinalize);
146
147 let on_finish = take_pipeline_on_finish();
148 let unsettled = unsettled_state_snapshot_async().await;
149
150 let pre_payload = serde_json::json!({
151 "event": HookEvent::PreFinish.as_str(),
152 "return_value": crate::llm::vm_value_to_json(&value),
153 "unsettled": unsettled.to_json(),
154 "has_on_finish": on_finish.is_some(),
155 });
156 self.fire_finish_lifecycle_event(HookEvent::PreFinish, &pre_payload)
157 .await?;
158
159 if !unsettled.is_empty() {
160 let payload = serde_json::json!({
161 "event": HookEvent::OnUnsettledDetected.as_str(),
162 "unsettled": unsettled.to_json(),
163 });
164 self.fire_finish_lifecycle_event(HookEvent::OnUnsettledDetected, &payload)
165 .await?;
166 }
167
168 let final_value = if let Some(closure) = on_finish {
169 let harness_value = crate::harness::Harness::real().into_vm_value();
170 self.call_closure_pub(&closure, &[harness_value, value])
171 .await?
172 } else {
173 value
174 };
175
176 let post_payload = serde_json::json!({
177 "event": HookEvent::PostFinish.as_str(),
178 "return_value": crate::llm::vm_value_to_json(&final_value),
179 "unsettled": unsettled.to_json(),
180 });
181 self.fire_finish_lifecycle_event(HookEvent::PostFinish, &post_payload)
182 .await?;
183
184 Ok(final_value)
185 }
186
187 async fn fire_finish_lifecycle_event(
205 &mut self,
206 event: crate::orchestration::HookEvent,
207 payload: &serde_json::Value,
208 ) -> Result<(), VmError> {
209 use crate::orchestration::{HookControl, HookEvent};
210 let invocations = crate::orchestration::matching_vm_lifecycle_hooks(event, payload);
211 if invocations.is_empty() {
212 return Ok(());
213 }
214 let mut current_payload = payload.clone();
215 for invocation in invocations {
216 let arg = crate::stdlib::json_to_vm_value(¤t_payload);
217 let closure = invocation.resolve(self).await?;
218 let raw = self.call_closure_pub(&closure, &[arg]).await?;
219 let (action, effects) = crate::orchestration::collect_hook_effects_and_action(
220 event,
221 raw,
222 crate::value::VmValue::Nil,
223 )?;
224 crate::orchestration::inject_hook_effects_into_current_session(effects)?;
225 let control = crate::orchestration::parse_hook_control_for_finish(event, &action)?;
226 match control {
227 HookControl::Allow => {}
228 HookControl::Block { reason } => {
229 if matches!(event, HookEvent::PreFinish) {
230 return Err(VmError::Runtime(format!(
231 "PreFinish hook returned block, which is not a valid control: {reason}. \
232 To delay pipeline finish until unsettled work clears, use \
233 OnFinish.block_until_settled (std/lifecycle) or return Modify/Allow \
234 from PreFinish."
235 )));
236 }
237 if matches!(event, HookEvent::PostFinish) {
238 continue;
240 }
241 return Err(VmError::Runtime(format!(
243 "{} hook blocked pipeline finish: {reason}",
244 event.as_str()
245 )));
246 }
247 HookControl::Modify { payload: modified } => {
248 current_payload = modified;
249 }
250 HookControl::Decision { .. } => {}
251 }
252 }
253 Ok(())
254 }
255
256 pub(crate) fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
258 if let Some(code) = error.process_exit_code() {
259 self.request_process_exit(code);
260 }
261 if error.is_uncatchable_control_flow() {
262 return Err(error);
263 }
264 let thrown_value = error.thrown_value();
265
266 if let Some(handler) = self.exception_handlers.pop() {
267 if let Some(error_type) = handler.error_type.as_deref() {
268 let matches = match &thrown_value {
270 VmValue::EnumVariant(enum_variant) => enum_variant.has_enum_name(error_type),
271 _ => false,
272 };
273 if !matches {
274 return self.handle_error(error);
275 }
276 }
277
278 self.release_sync_guards_after_unwind(handler.frame_depth, handler.env_scope_depth);
279
280 while self.frames.len() > handler.frame_depth {
281 if let Some(frame) = self.frames.pop() {
282 if let Some(ref dir) = frame.saved_source_dir {
283 crate::stdlib::set_thread_source_dir(dir);
284 }
285 self.iterators.truncate(frame.saved_iterator_depth);
286 self.env = frame.saved_env;
287 }
288 }
289 crate::step_runtime::prune_below_frame(self.frames.len());
290
291 while self
293 .deadlines
294 .last()
295 .is_some_and(|d| d.1 > handler.frame_depth)
296 {
297 self.deadlines.pop();
298 }
299
300 self.env.truncate_scopes(handler.env_scope_depth);
301
302 self.stack.truncate(handler.stack_depth);
303 self.stack.push(thrown_value);
304
305 if let Some(frame) = self.frames.last_mut() {
306 frame.ip = handler.catch_ip;
307 }
308
309 Ok(None)
310 } else {
311 Err(error)
312 }
313 }
314
315 pub(crate) async fn run_chunk(&mut self, chunk: ChunkRef) -> Result<VmValue, VmError> {
316 self.run_chunk_ref(chunk, 0, None, None, None, None).await
317 }
318
319 pub(crate) async fn run_chunk_ref(
320 &mut self,
321 chunk: ChunkRef,
322 argc: usize,
323 saved_source_dir: Option<std::path::PathBuf>,
324 module_functions: Option<ModuleFunctionRegistry>,
325 module_state: Option<crate::value::ModuleState>,
326 local_slots: Option<Vec<LocalSlot>>,
327 ) -> Result<VmValue, VmError> {
328 self.ensure_execution_available()?;
329 let debugger = self.debugger_attached();
330 let local_slots = local_slots.unwrap_or_else(|| Self::fresh_local_slots(&chunk));
331 let initial_env = if debugger {
332 Some(self.env.clone())
333 } else {
334 None
335 };
336 let initial_local_slots = if debugger {
337 Some(local_slots.clone())
338 } else {
339 None
340 };
341 let inline_cache_set = self.inline_cache_set_index_for_chunk(&chunk);
342 self.frames.push(CallFrame {
343 chunk,
344 inline_cache_set,
345 ip: 0,
346 stack_base: self.stack.len(),
347 saved_env: self.env.clone(),
348 initial_env,
349 initial_local_slots,
350 saved_iterator_depth: self.iterators.len(),
351 fn_name: String::new(),
352 argc,
353 saved_source_dir,
354 module_functions,
355 module_state,
356 local_slots,
357 local_scope_base: self.env.scope_depth().saturating_sub(1),
358 local_scope_depth: 0,
359 });
360
361 self.drive_dispatch_loop(0, false).await
362 }
363
364 pub(crate) async fn drive_until_frame_depth(
371 &mut self,
372 target_depth: usize,
373 ) -> Result<VmValue, VmError> {
374 self.drive_dispatch_loop(target_depth, true).await
375 }
376
377 async fn drive_dispatch_loop(
391 &mut self,
392 target_depth: usize,
393 restore_on_final_pop: bool,
394 ) -> Result<VmValue, VmError> {
395 self.ensure_execution_available()?;
396 let _task_activity = self
397 .wait_for_graph
398 .register_task(self.runtime_context.task_id.clone());
399 loop {
400 if !self.scope_interrupts_clean() {
407 if let Some(err) = self.pending_scope_interrupt().await {
408 match self.handle_error(err) {
409 Ok(None) => continue,
410 Ok(Some(val)) => return Ok(val),
411 Err(e) => {
412 self.unwind_frames_to_depth(target_depth);
413 return Err(e);
414 }
415 }
416 }
417 }
418
419 let frame = match self.frames.last_mut() {
420 Some(f) => f,
421 None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
422 };
423
424 if frame.ip >= frame.chunk.code.len() {
425 let val = self.stack.pop().unwrap_or(VmValue::Nil);
426 let val = self.run_step_post_hooks_for_current_frame(val).await?;
427 self.release_sync_guards_for_frame(self.frames.len());
428 let popped_frame = self.frames.pop().unwrap();
429 if let Some(ref dir) = popped_frame.saved_source_dir {
430 crate::stdlib::set_thread_source_dir(dir);
431 }
432 let current_depth = self.frames.len();
433 crate::step_runtime::prune_below_frame(current_depth);
434 while self.deadlines.last().is_some_and(|d| d.1 > current_depth) {
439 self.deadlines.pop();
440 }
441
442 let reached_target = current_depth <= target_depth;
443 if reached_target && !restore_on_final_pop {
444 return Ok(val);
447 }
448 self.iterators.truncate(popped_frame.saved_iterator_depth);
449 self.env = popped_frame.saved_env;
450 self.stack.truncate(popped_frame.stack_base);
451 if reached_target {
452 return Ok(val);
453 }
454 self.stack.push(val);
455 continue;
456 }
457
458 let op_byte = frame.chunk.code[frame.ip];
459 if let Some(coverage) = self.coverage.as_mut() {
465 coverage.record(&frame.chunk, frame.ip);
466 }
467 frame.ip += 1;
468
469 let op = match Op::from_byte(op_byte) {
474 Some(op) => op,
475 None => return Err(VmError::InvalidInstruction(op_byte)),
476 };
477 let op_result: Result<(), VmError> = if let Some(result) = self.execute_op_sync(op) {
478 result
479 } else if self.scope_interrupts_clean() {
480 self.execute_op_async(op).await
481 } else {
482 match self.execute_op_with_scope_interrupts(op_byte).await {
483 Ok(Some(val)) => return Ok(val),
484 Ok(None) => Ok(()),
485 Err(e) => Err(e),
486 }
487 };
488
489 match op_result {
490 Ok(()) => continue,
491 Err(VmError::Return(val)) => {
492 let val = self.run_step_post_hooks_for_current_frame(val).await?;
493 if let Some(popped_frame) = self.frames.pop() {
494 self.release_sync_guards_for_frame(self.frames.len() + 1);
495 if let Some(ref dir) = popped_frame.saved_source_dir {
496 crate::stdlib::set_thread_source_dir(dir);
497 }
498 let current_depth = self.frames.len();
499 self.exception_handlers
500 .retain(|h| h.frame_depth <= current_depth);
501 crate::step_runtime::prune_below_frame(current_depth);
502 while self.deadlines.last().is_some_and(|d| d.1 > current_depth) {
503 self.deadlines.pop();
504 }
505
506 let reached_target = current_depth <= target_depth;
507 if reached_target && !restore_on_final_pop {
508 return Ok(val);
509 }
510 self.iterators.truncate(popped_frame.saved_iterator_depth);
511 self.env = popped_frame.saved_env;
512 self.stack.truncate(popped_frame.stack_base);
513 if reached_target {
514 return Ok(val);
515 }
516 self.stack.push(val);
517 } else {
518 return Ok(val);
519 }
520 }
521 Err(e) => {
522 if self.error_stack_trace.is_empty() {
524 self.error_stack_trace = self.capture_stack_trace();
525 }
526 let e = match self.apply_step_error_boundary(e) {
533 StepBoundaryOutcome::Returned(val) => {
534 self.error_stack_trace.clear();
535 if self.frames.len() <= target_depth {
536 return Ok(val);
537 }
538 self.stack.push(val);
539 continue;
540 }
541 StepBoundaryOutcome::Throw(err) => err,
542 };
543 match self.handle_error(e) {
544 Ok(None) => {
545 self.error_stack_trace.clear();
546 continue;
547 }
548 Ok(Some(val)) => return Ok(val),
549 Err(e) => {
550 self.unwind_frames_to_depth(target_depth);
551 return Err(self.enrich_error_with_line(e));
552 }
553 }
554 }
555 }
556 }
557 }
558
559 fn unwind_frames_to_depth(&mut self, target_depth: usize) {
566 while self.frames.len() > target_depth {
567 let frame_depth = self.frames.len();
568 if let Some(frame) = self.frames.pop() {
569 self.release_sync_guards_for_frame(frame_depth);
570 if let Some(ref dir) = frame.saved_source_dir {
571 crate::stdlib::set_thread_source_dir(dir);
572 }
573 self.iterators.truncate(frame.saved_iterator_depth);
574 self.env = frame.saved_env;
575 self.stack.truncate(frame.stack_base);
576 }
577 }
578 let current_depth = self.frames.len();
579 crate::step_runtime::prune_below_frame(current_depth);
580 while self.deadlines.last().is_some_and(|d| d.1 > current_depth) {
581 self.deadlines.pop();
582 }
583 }
584
585 pub(crate) fn apply_step_error_boundary(&mut self, error: VmError) -> StepBoundaryOutcome {
591 use crate::step_runtime;
592 if !step_runtime::is_step_budget_exhausted(&error) {
593 return StepBoundaryOutcome::Throw(error);
594 }
595 let Some(step_depth) = step_runtime::active_step_frame_depth() else {
596 return StepBoundaryOutcome::Throw(error);
597 };
598 if step_depth != self.frames.len() {
603 return StepBoundaryOutcome::Throw(error);
604 }
605 let boundary = step_runtime::with_active_step(|step| step.definition.boundary())
606 .unwrap_or(step_runtime::StepErrorBoundary::Fail);
607 match boundary {
608 step_runtime::StepErrorBoundary::Continue => {
609 if let Some(popped) = self.frames.pop() {
613 self.release_sync_guards_for_frame(self.frames.len() + 1);
614 if let Some(ref dir) = popped.saved_source_dir {
615 crate::stdlib::set_thread_source_dir(dir);
616 }
617 let current_depth = self.frames.len();
618 self.exception_handlers
619 .retain(|h| h.frame_depth <= current_depth);
620 step_runtime::pop_and_record(
621 current_depth + 1,
622 "skipped",
623 Some(step_runtime_error_message(&error)),
624 );
625 if self.frames.is_empty() {
626 return StepBoundaryOutcome::Returned(VmValue::Nil);
627 }
628 self.iterators.truncate(popped.saved_iterator_depth);
629 self.env = popped.saved_env;
630 self.stack.truncate(popped.stack_base);
631 }
632 StepBoundaryOutcome::Returned(VmValue::Nil)
633 }
634 step_runtime::StepErrorBoundary::Escalate => {
635 let identity = step_runtime::with_active_step(|step| {
636 (
637 step.definition.name.clone(),
638 step.definition.function.clone(),
639 )
640 });
641 step_runtime::pop_and_record(
642 step_depth,
643 "escalated",
644 Some(step_runtime_error_message(&error)),
645 );
646 let (step_name, function) = identity.unzip();
647 StepBoundaryOutcome::Throw(step_runtime::mark_escalated(
648 error,
649 step_name.as_deref(),
650 function.as_deref(),
651 ))
652 }
653 step_runtime::StepErrorBoundary::Fail => {
654 step_runtime::pop_and_record(
655 step_depth,
656 "failed",
657 Some(step_runtime_error_message(&error)),
658 );
659 StepBoundaryOutcome::Throw(error)
660 }
661 }
662 }
663}
664
665fn next_deadline(
666 execution_deadline: Option<Instant>,
667 scope_deadline: Option<Instant>,
668 interrupt_handler_deadline: Option<Instant>,
669) -> (Option<Instant>, Option<DeadlineKind>) {
670 [
671 (execution_deadline, DeadlineKind::Execution),
672 (scope_deadline, DeadlineKind::Scope),
673 (interrupt_handler_deadline, DeadlineKind::InterruptHandler),
674 ]
675 .into_iter()
676 .filter_map(|(deadline, kind)| deadline.map(|deadline| (deadline, kind)))
677 .min_by_key(|(deadline, _)| *deadline)
678 .map_or((None, None), |(deadline, kind)| {
679 (Some(deadline), Some(kind))
680 })
681}
682
683fn step_runtime_error_message(error: &VmError) -> String {
684 match error {
685 VmError::Thrown(VmValue::Dict(dict)) => dict
686 .get("message")
687 .map(|v| v.display())
688 .unwrap_or_else(|| error.to_string()),
689 _ => error.to_string(),
690 }
691}
692
693pub(crate) enum StepBoundaryOutcome {
694 Returned(VmValue),
695 Throw(VmError),
696}
697
698impl crate::vm::Vm {
699 pub(crate) async fn execute_one_cycle(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
700 if let Some(err) = self.pending_scope_interrupt().await {
701 match self.handle_error(err) {
702 Ok(None) => return Ok(None),
703 Ok(Some(val)) => return Ok(Some((val, false))),
704 Err(e) => return Err(e),
705 }
706 }
707
708 let frame = match self.frames.last_mut() {
709 Some(f) => f,
710 None => {
711 let val = self.stack.pop().unwrap_or(VmValue::Nil);
712 return Ok(Some((val, false)));
713 }
714 };
715
716 if frame.ip >= frame.chunk.code.len() {
717 let val = self.stack.pop().unwrap_or(VmValue::Nil);
718 self.release_sync_guards_for_frame(self.frames.len());
719 let popped_frame = self.frames.pop().unwrap();
720 if self.frames.is_empty() {
721 return Ok(Some((val, false)));
722 }
723 self.iterators.truncate(popped_frame.saved_iterator_depth);
724 self.env = popped_frame.saved_env;
725 self.stack.truncate(popped_frame.stack_base);
726 self.stack.push(val);
727 return Ok(None);
728 }
729
730 let op = frame.chunk.code[frame.ip];
731 frame.ip += 1;
732
733 match self.execute_op_with_scope_interrupts(op).await {
734 Ok(Some(val)) => Ok(Some((val, false))),
735 Ok(None) => Ok(None),
736 Err(VmError::Return(val)) => {
737 if let Some(popped_frame) = self.frames.pop() {
738 self.release_sync_guards_for_frame(self.frames.len() + 1);
739 if let Some(ref dir) = popped_frame.saved_source_dir {
740 crate::stdlib::set_thread_source_dir(dir);
741 }
742 let current_depth = self.frames.len();
743 self.exception_handlers
744 .retain(|h| h.frame_depth <= current_depth);
745 if self.frames.is_empty() {
746 return Ok(Some((val, false)));
747 }
748 self.iterators.truncate(popped_frame.saved_iterator_depth);
749 self.env = popped_frame.saved_env;
750 self.stack.truncate(popped_frame.stack_base);
751 self.stack.push(val);
752 Ok(None)
753 } else {
754 Ok(Some((val, false)))
755 }
756 }
757 Err(e) => {
758 if self.error_stack_trace.is_empty() {
759 self.error_stack_trace = self.capture_stack_trace();
760 }
761 match self.handle_error(e) {
762 Ok(None) => {
763 self.error_stack_trace.clear();
764 Ok(None)
765 }
766 Ok(Some(val)) => Ok(Some((val, false))),
767 Err(e) => Err(self.enrich_error_with_line(e)),
768 }
769 }
770 }
771 }
772
773 async fn execute_op_with_scope_interrupts(
774 &mut self,
775 op: u8,
776 ) -> Result<Option<VmValue>, VmError> {
777 #[cfg(test)]
778 SCOPE_INTERRUPT_ASYNC_DISPATCHES
779 .set(SCOPE_INTERRUPT_ASYNC_DISPATCHES.get().saturating_add(1));
780
781 enum ScopeInterruptResult {
782 Op(Result<Option<VmValue>, VmError>),
783 Deadline(DeadlineKind),
784 CancelTimedOut,
785 }
786
787 let (deadline, deadline_kind) = next_deadline(
788 self.execution_deadline.current(),
789 self.deadlines.last().map(|(deadline, _)| *deadline),
790 self.interrupt_handler_deadline,
791 );
792 let cancel_token = self.cancel_token.clone();
793
794 if deadline.is_none() && cancel_token.is_none() {
795 return self.execute_op(op).await;
796 }
797
798 let has_deadline = deadline.is_some();
799 let cancel_requested_at_start = cancel_token
800 .as_ref()
801 .is_some_and(|token| token.load(std::sync::atomic::Ordering::SeqCst));
802 let has_cancel = cancel_token.is_some() && !cancel_requested_at_start;
803 let deadline_sleep = async move {
804 if let Some(deadline) = deadline {
805 tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)).await;
806 } else {
807 std::future::pending::<()>().await;
808 }
809 };
810 let cancel_sleep = async move {
811 if let Some(token) = cancel_token {
812 while !token.load(std::sync::atomic::Ordering::SeqCst) {
813 tokio::time::sleep(Duration::from_millis(10)).await;
814 }
815 } else {
816 std::future::pending::<()>().await;
817 }
818 };
819
820 let result = {
821 let op_future = self.execute_op(op);
822 tokio::pin!(op_future);
823 tokio::select! {
824 result = &mut op_future => ScopeInterruptResult::Op(result),
825 _ = deadline_sleep, if has_deadline => {
826 ScopeInterruptResult::Deadline(deadline_kind.unwrap_or(DeadlineKind::Scope))
827 },
828 _ = cancel_sleep, if has_cancel => {
829 let grace = tokio::time::sleep(CANCEL_GRACE_ASYNC_OP);
830 tokio::pin!(grace);
831 tokio::select! {
832 result = &mut op_future => ScopeInterruptResult::Op(result),
833 _ = &mut grace => ScopeInterruptResult::CancelTimedOut,
834 }
835 }
836 }
837 };
838
839 match result {
840 ScopeInterruptResult::Op(result) => result,
841 ScopeInterruptResult::Deadline(DeadlineKind::Execution) => {
842 self.cancel_spawned_tasks();
843 Err(VmError::ExecutionDeadlineExceeded)
844 }
845 ScopeInterruptResult::Deadline(DeadlineKind::Scope) => {
846 self.deadlines.pop();
847 self.cancel_spawned_tasks();
848 Err(Self::deadline_exceeded_error())
849 }
850 ScopeInterruptResult::Deadline(DeadlineKind::InterruptHandler) => {
851 Err(Self::interrupt_handler_timeout_error())
852 }
853 ScopeInterruptResult::CancelTimedOut => {
854 self.cancel_spawned_tasks();
855 let signal = self
856 .take_host_interrupt_signal()
857 .unwrap_or_else(|| "SIGINT".to_string());
858 if self.has_interrupt_handler_for(&signal) {
859 self.dispatch_interrupt_handlers(&signal).await?;
860 }
861 Err(Self::cancelled_error())
862 }
863 }
864 }
865
866 pub(crate) fn deadline_exceeded_error() -> VmError {
867 VmError::Thrown(VmValue::String(arcstr::ArcStr::from("Deadline exceeded")))
868 }
869
870 pub(crate) fn cancelled_error() -> VmError {
871 VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
872 "kind:cancelled:VM cancelled by host",
873 )))
874 }
875
876 pub(crate) fn capture_stack_trace(&self) -> Vec<(String, usize, usize, Option<String>)> {
878 self.frames
879 .iter()
880 .map(|f| {
881 let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
882 let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
883 let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
884 (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
885 })
886 .collect()
887 }
888
889 pub(crate) fn enrich_error_with_line(&self, error: VmError) -> VmError {
893 let (line, file) = self
898 .error_stack_trace
899 .last()
900 .map(|(_, l, _, f)| (*l, f.clone()))
901 .unwrap_or_else(|| (self.current_line(), None));
902 if line == 0 {
903 return error;
904 }
905 let suffix = match file.as_deref() {
906 Some(path) => {
907 let name = std::path::Path::new(path)
908 .file_name()
909 .and_then(|n| n.to_str())
910 .unwrap_or(path);
911 format!(" ({name}:{line})")
912 }
913 None => format!(" (line {line})"),
914 };
915 match error {
916 VmError::Runtime(msg) => VmError::Runtime(format!("{msg}{suffix}")),
917 VmError::TypeError(msg) => VmError::TypeError(format!("{msg}{suffix}")),
918 VmError::DivisionByZero => VmError::Runtime(format!("Division by zero{suffix}")),
919 VmError::UndefinedVariable(name) => {
920 VmError::Runtime(format!("Undefined variable: {name}{suffix}"))
921 }
922 VmError::UndefinedBuiltin(name) => {
923 VmError::Runtime(format!("Undefined builtin: {name}{suffix}"))
924 }
925 VmError::ImmutableAssignment(name) => VmError::Runtime(format!(
926 "Cannot assign to immutable binding: {name}{suffix}"
927 )),
928 VmError::StackOverflow => {
929 VmError::Runtime(format!("Stack overflow: too many nested calls{suffix}"))
930 }
931 other => other,
937 }
938 }
939}
940
941#[cfg(test)]
942mod tests {
943 use super::*;
944 use crate::compiler::Compiler;
945 use crate::stdlib::register_vm_stdlib;
946 use harn_lexer::Lexer;
947 use harn_parser::Parser;
948 use std::sync::atomic::{AtomicBool, Ordering};
949
950 fn compile_harn(source: &str) -> Chunk {
951 let mut lexer = Lexer::new(source);
952 let tokens = lexer.tokenize().unwrap();
953 let mut parser = Parser::new(tokens);
954 let program = parser.parse().unwrap();
955 Compiler::new().compile(&program).unwrap()
956 }
957
958 #[tokio::test(flavor = "current_thread")]
959 async fn dropping_timed_execution_restores_ambient_state_and_poison_vm_reuse() {
960 let local = tokio::task::LocalSet::new();
961 local
962 .run_until(async {
963 crate::reset_thread_local_state();
964 let baseline_dir = tempfile::tempdir().unwrap();
965 let imported_dir = tempfile::tempdir().unwrap();
966 let poisoned_dir = tempfile::tempdir().unwrap();
967 let quick = compile_harn("pipeline default() { return 42 }");
968 let mut vm = Vm::new();
969 register_vm_stdlib(&mut vm);
970 crate::tracing::set_tracing_enabled(true);
971
972 let child_started = Arc::new(AtomicBool::new(false));
973 let child_effect = Arc::new(AtomicBool::new(false));
974 let child_release = Arc::new(tokio::sync::Notify::new());
975 let (ambient_started_tx, ambient_started_rx) = tokio::sync::oneshot::channel();
976 let ambient_started_tx = Arc::new(std::sync::Mutex::new(Some(ambient_started_tx)));
977 let started_for_builtin = Arc::clone(&child_started);
978 vm.register_builtin("child_started", move |_args, _output| {
979 started_for_builtin.store(true, Ordering::Release);
980 Ok(VmValue::Nil)
981 });
982 let effect_for_builtin = Arc::clone(&child_effect);
983 vm.register_builtin("child_effect", move |_args, _output| {
984 effect_for_builtin.store(true, Ordering::Release);
985 Ok(VmValue::Nil)
986 });
987 let release_for_builtin = Arc::clone(&child_release);
988 vm.register_async_builtin("wait_for_child_release", move |_ctx, _args| {
989 let release = Arc::clone(&release_for_builtin);
990 async move {
991 release.notified().await;
992 Ok(VmValue::Nil)
993 }
994 });
995 vm.register_async_builtin("wait_forever", |_ctx, _args| async move {
996 std::future::pending::<()>().await;
997 Ok(VmValue::Nil)
998 });
999
1000 let imported_path = imported_dir.path().join("cancelled.harn");
1001 let imported_source_dir = imported_dir.path().to_path_buf();
1002 let poisoned_source_dir = poisoned_dir.path().to_path_buf();
1003 let ambient_started_for_builtin = Arc::clone(&ambient_started_tx);
1004 vm.register_builtin("strand_ambient_state", move |_args, _output| {
1005 crate::step_runtime::register_persona(
1006 "cancellation_entry",
1007 crate::step_runtime::PersonaDefinition {
1008 name: "cancel_persona".into(),
1009 stages: vec![crate::personas::StageDecl {
1010 name: "cancel_step".into(),
1011 allowed_tools: Some(vec!["cancel_tool".into()]),
1012 ..Default::default()
1013 }],
1014 ..Default::default()
1015 },
1016 );
1017 crate::step_runtime::register_step(
1018 "cancelled_step",
1019 crate::step_runtime::StepDefinition {
1020 name: "cancel_step".into(),
1021 function: "cancelled_step".into(),
1022 model: Some("cancel-model".into()),
1023 ..Default::default()
1024 },
1025 );
1026 assert!(crate::step_runtime::maybe_push_active_persona(
1027 "cancellation_entry",
1028 1
1029 ));
1030 assert!(crate::step_runtime::maybe_push_active_step(
1031 "cancelled_step",
1032 2,
1033 &[]
1034 ));
1035 assert_eq!(
1036 crate::stdlib::process::source_root_path(),
1037 imported_source_dir
1038 );
1039 assert_eq!(
1040 crate::step_runtime::current_persona_name().as_deref(),
1041 Some("cancel_persona")
1042 );
1043 assert_eq!(
1044 crate::step_runtime::active_step_model_default().as_deref(),
1045 Some("cancel-model")
1046 );
1047 assert_eq!(
1048 crate::orchestration::current_execution_policy()
1049 .unwrap()
1050 .tools,
1051 vec!["cancel_tool"]
1052 );
1053 crate::stdlib::process::set_thread_source_dir(&poisoned_source_dir);
1054 crate::stdlib::process::set_thread_execution_context(Some(
1055 crate::orchestration::RunExecutionRecord {
1056 adapter: Some("cancelled".into()),
1057 ..Default::default()
1058 },
1059 ));
1060 crate::orchestration::push_approval_policy(
1061 crate::orchestration::ToolApprovalPolicy {
1062 auto_deny: vec!["cancel_tool".into()],
1063 ..Default::default()
1064 },
1065 );
1066 if let Some(sender) = ambient_started_for_builtin.lock().unwrap().take() {
1067 let _ = sender.send(());
1068 }
1069 Ok(VmValue::Nil)
1070 });
1071
1072 std::fs::write(
1073 &imported_path,
1074 r#"
1075@persona(name: "cancel_persona", stages: [{name: "cancel_step", allowed_tools: ["cancel_tool"]}])
1076pub fn cancellation_entry() {
1077 return cancelled_step()
1078}
1079
1080@step(name: "cancel_step", model: "cancel-model")
1081fn cancelled_step() {
1082 pipeline_on_finish({ _h, value -> value })
1083 const child = spawn {
1084 child_started()
1085 wait_for_child_release()
1086 child_effect()
1087 }
1088 strand_ambient_state()
1089 wait_forever()
1090}
1091"#,
1092 )
1093 .unwrap();
1094 let mut helper_exports = vm
1095 .load_module_exports_from_source(
1096 "<cancellation-helper>",
1097 "pub fn outer_callback(_h, value) { return value }\n\
1098 pub fn answer() { return 42 }",
1099 )
1100 .await
1101 .unwrap();
1102 let callable = helper_exports.remove("answer").unwrap();
1103 let outer_callback = helper_exports.remove("outer_callback").unwrap();
1104 let slow = compile_harn(&format!(
1105 "import {{ cancellation_entry }} from \"{}\"\n\
1106 pipeline default() {{ return cancellation_entry() }}",
1107 imported_path.display()
1108 ));
1109
1110 let baseline_execution = crate::orchestration::RunExecutionRecord {
1111 cwd: Some(baseline_dir.path().display().to_string()),
1112 source_dir: Some(baseline_dir.path().display().to_string()),
1113 adapter: Some("baseline".into()),
1114 ..Default::default()
1115 };
1116 let baseline_policy = crate::orchestration::CapabilityPolicy {
1117 tools: vec!["baseline_tool".into(), "cancel_tool".into()],
1118 ..Default::default()
1119 };
1120 let baseline_approval = crate::orchestration::ToolApprovalPolicy {
1121 auto_approve: vec!["baseline_tool".into()],
1122 ..Default::default()
1123 };
1124 crate::stdlib::process::set_thread_source_dir(baseline_dir.path());
1125 crate::stdlib::process::set_thread_execution_context(Some(
1126 baseline_execution.clone(),
1127 ));
1128 crate::orchestration::push_execution_policy(baseline_policy.clone());
1129 crate::orchestration::push_approval_policy(baseline_approval.clone());
1130 crate::orchestration::set_pipeline_on_finish(Arc::clone(&outer_callback));
1131 let outer_span =
1132 crate::tracing::span_start(crate::tracing::SpanKind::Pipeline, "outer".into());
1133
1134 let mut execution =
1135 Box::pin(vm.execute_with_timeout(&slow, Duration::from_secs(30)));
1136 tokio::select! {
1137 biased;
1138 result = &mut execution => panic!("slow execution unexpectedly finished: {result:?}"),
1139 started = async {
1140 ambient_started_rx.await.expect("step did not reach cancellation point");
1141 while !child_started.load(Ordering::Acquire) {
1142 tokio::task::yield_now().await;
1143 }
1144 } => started,
1145 }
1146 drop(execution);
1147
1148 assert!(!vm.execution_deadline.is_active());
1149 assert!(!vm.frames.is_empty(), "fixture must abandon a live frame");
1150 assert_eq!(
1151 crate::stdlib::process::source_root_path(),
1152 baseline_dir.path()
1153 );
1154 assert_eq!(
1155 crate::stdlib::process::current_execution_context(),
1156 Some(baseline_execution.clone())
1157 );
1158 assert_eq!(
1159 crate::orchestration::current_execution_policy(),
1160 Some(baseline_policy.clone())
1161 );
1162 assert_eq!(
1163 crate::orchestration::current_approval_policy(),
1164 Some(baseline_approval.clone())
1165 );
1166 assert!(crate::step_runtime::current_persona_name().is_none());
1167 assert!(crate::step_runtime::active_step_model_default().is_none());
1168 assert_eq!(crate::tracing::current_span_id(), Some(outer_span));
1169 let restored_callback =
1170 crate::orchestration::take_pipeline_on_finish().unwrap();
1171 assert!(Arc::ptr_eq(&restored_callback, &outer_callback));
1172 crate::orchestration::set_pipeline_on_finish(restored_callback);
1173 let abandoned_spans = crate::tracing::peek_spans();
1174 assert!(abandoned_spans.iter().any(|span| {
1175 span.name == "cancel_step"
1176 && span.metadata.get("status") == Some(&serde_json::json!("abandoned"))
1177 }));
1178 assert!(abandoned_spans.iter().any(|span| {
1179 span.name == "main"
1180 && span.metadata.get("status") == Some(&serde_json::json!("abandoned"))
1181 }));
1182
1183 let frame_depth = vm.frames.len();
1184 let output = vm.output().to_string();
1185 let error = vm.execute(&quick).await.unwrap_err();
1186 assert!(matches!(error, VmError::AbandonedExecution));
1187 let closure_error = vm.call_closure_pub(&callable, &[]).await.unwrap_err();
1188 assert!(matches!(closure_error, VmError::AbandonedExecution));
1189 let source_cache_len = vm.source_cache.len();
1190 let module_cache_len = vm.module_cache.len();
1191 let module_error = vm
1192 .load_module_exports_from_source(
1193 "<poisoned-module-load>",
1194 "pub fn poisoned() { return 0 }",
1195 )
1196 .await
1197 .unwrap_err();
1198 assert!(matches!(module_error, VmError::AbandonedExecution));
1199 assert_eq!(vm.source_cache.len(), source_cache_len);
1200 assert_eq!(vm.module_cache.len(), module_cache_len);
1201 let start_error = vm.start(&quick).unwrap_err();
1202 assert!(matches!(start_error, VmError::AbandonedExecution));
1203 let restart_error = vm.restart_frame(0).unwrap_err();
1204 assert!(matches!(restart_error, VmError::AbandonedExecution));
1205 assert_eq!(vm.frames.len(), frame_depth);
1206 assert_eq!(vm.output(), output);
1207
1208 let mut vm_b = Vm::new();
1209 register_vm_stdlib(&mut vm_b);
1210 let observed_callback = Arc::clone(&outer_callback);
1211 let observed_dir = baseline_dir.path().to_path_buf();
1212 vm_b.register_builtin("observe_baseline", move |_args, _output| {
1213 assert_eq!(crate::stdlib::process::source_root_path(), observed_dir);
1214 assert_eq!(
1215 crate::stdlib::process::current_execution_context(),
1216 Some(baseline_execution.clone())
1217 );
1218 assert_eq!(
1219 crate::orchestration::current_execution_policy(),
1220 Some(baseline_policy.clone())
1221 );
1222 assert_eq!(
1223 crate::orchestration::current_approval_policy(),
1224 Some(baseline_approval.clone())
1225 );
1226 assert!(crate::step_runtime::current_persona_name().is_none());
1227 assert!(crate::step_runtime::active_step_model_default().is_none());
1228 let callback = crate::orchestration::take_pipeline_on_finish().unwrap();
1229 assert!(Arc::ptr_eq(&callback, &observed_callback));
1230 crate::orchestration::set_pipeline_on_finish(callback);
1231 Ok(VmValue::Nil)
1232 });
1233 let vm_b_chunk =
1234 compile_harn("pipeline default() { observe_baseline(); return 42 }");
1235 assert!(matches!(
1236 vm_b.execute(&vm_b_chunk).await.unwrap(),
1237 VmValue::Int(42)
1238 ));
1239 assert_eq!(crate::tracing::current_span_id(), Some(outer_span));
1240
1241 drop(vm);
1242 child_release.notify_one();
1243 for _ in 0..10 {
1244 tokio::task::yield_now().await;
1245 }
1246 assert!(
1247 !child_effect.load(Ordering::Acquire),
1248 "dropping an abandoned VM must abort spawned side effects"
1249 );
1250 crate::reset_thread_local_state();
1251 })
1252 .await;
1253 }
1254
1255 #[tokio::test(flavor = "current_thread")]
1256 async fn natural_host_deadline_is_terminal_not_abandoned() {
1257 let local = tokio::task::LocalSet::new();
1258 local
1259 .run_until(async {
1260 let infinite = compile_harn("pipeline default() { while true {} }");
1261 let quick = compile_harn("pipeline default() { return 42 }");
1262 let mut vm = Vm::new();
1263 register_vm_stdlib(&mut vm);
1264
1265 let error = vm
1266 .execute_with_timeout(&infinite, Duration::ZERO)
1267 .await
1268 .unwrap_err();
1269 assert!(matches!(error, VmError::ExecutionDeadlineExceeded));
1270 assert!(!vm.execution_deadline.is_abandoned());
1271 assert!(matches!(
1272 vm.execute(&quick).await.unwrap(),
1273 VmValue::Int(42)
1274 ));
1275 })
1276 .await;
1277 }
1278
1279 #[tokio::test(flavor = "current_thread")]
1280 async fn timed_finite_loop_keeps_sync_opcodes_on_direct_dispatch() {
1281 let local = tokio::task::LocalSet::new();
1282 local
1283 .run_until(async {
1284 let chunk = compile_harn(
1285 r"
1286pipeline default() {
1287 let total = 0
1288 for i in 0 to 10000 {
1289 total = total + i
1290 }
1291 return total
1292}
1293",
1294 );
1295 let mut vm = Vm::new();
1296 register_vm_stdlib(&mut vm);
1297 reset_scope_interrupt_async_dispatches();
1298
1299 let value = vm
1300 .execute_with_timeout(&chunk, Duration::from_secs(1))
1301 .await
1302 .unwrap();
1303
1304 assert!(matches!(value, VmValue::Int(_)));
1305 assert!(
1306 scope_interrupt_async_dispatches() <= 4,
1307 "finite sync loop fell back to per-op async dispatch"
1308 );
1309 })
1310 .await;
1311 }
1312}