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