1use std::cell::Cell;
2use std::fmt;
3use std::rc::Rc;
4use std::time::Duration;
5
6use crate::cycle::GcEdge;
7use crate::{Env, EvalContext, SemaError, Value};
8
9use super::{
10 CancelReason, ChannelId, PreparedExternalOperation, PromiseId, ResourceGateId,
11 TaskContextHandle, TaskOutcome, TaskSettlement, Trace,
12};
13
14#[derive(Clone, Debug, Default, Eq, PartialEq)]
15pub struct CancellationView {
16 requested: bool,
17 reason: Option<CancelReason>,
18}
19
20impl CancellationView {
21 #[doc(hidden)]
22 pub fn new(requested: bool, reason: Option<CancelReason>) -> Self {
23 Self { requested, reason }
24 }
25
26 pub fn is_requested(&self) -> bool {
27 self.requested
28 }
29
30 pub fn reason(&self) -> Option<&CancelReason> {
31 self.reason.as_ref()
32 }
33}
34
35pub struct NativeCallContext<'a> {
36 pub eval_context: &'a EvalContext,
37 pub task_context: TaskContextHandle,
38 pub call_env: Option<Rc<Env>>,
39 pub cancellation: CancellationView,
40 pub hof_host: Option<&'a dyn SyncHofHost>,
45}
46
47pub trait SyncHofHost {
57 fn run_sync_hof(
58 &self,
59 callable: &Value,
60 driver: &mut dyn FnMut(&mut dyn SyncCallbackSession) -> NativeResult,
61 ) -> Option<NativeResult>;
62}
63
64pub trait SyncCallbackSession {
67 fn call_owned(&mut self, args: &mut [Value]) -> Result<Value, SemaError>;
71
72 fn should_yield(&self) -> bool;
76}
77
78pub type NativeResult = Result<NativeOutcome, SemaError>;
79
80pub enum NativeOutcome {
81 Return(Value),
82 Call(NativeCall),
83 Suspend(NativeSuspend),
84 Runtime(RuntimeRequest),
85}
86
87pub enum RuntimeRequest {
88 Spawn {
89 callable: Value,
90 continuation: Box<dyn NativeContinuation>,
91 },
92 CancelPromise {
93 promise: PromiseId,
94 continuation: Box<dyn NativeContinuation>,
95 },
96 CreateChannel {
97 capacity: usize,
98 continuation: Box<dyn NativeContinuation>,
99 },
100 ChannelOp {
101 channel: ChannelId,
102 operation: ChannelOperation,
103 continuation: Box<dyn NativeContinuation>,
104 },
105 CreateSettledPromise {
106 outcome: TaskOutcome,
107 continuation: Box<dyn NativeContinuation>,
108 },
109 InspectPromise {
110 promise: PromiseId,
111 continuation: Box<dyn NativeContinuation>,
112 },
113 PromiseSetWait {
114 wait: PromiseSetWait,
115 continuation: Box<dyn NativeContinuation>,
116 },
117 OriginBarrier {
118 continuation: Box<dyn NativeContinuation>,
119 },
120 CreateResourceGate {
127 continuation: Box<dyn NativeContinuation>,
128 },
129 ReleaseResourceGate {
133 gate: ResourceGateId,
134 continuation: Box<dyn NativeContinuation>,
135 },
136 CloseResourceGate {
140 gate: ResourceGateId,
141 continuation: Box<dyn NativeContinuation>,
142 },
143}
144
145pub enum ChannelOperation {
146 Close,
147 TryReceive,
148 Inspect(ChannelQuery),
149}
150#[derive(Clone, Copy, Debug)]
151pub enum ChannelQuery {
152 Closed,
153 Count,
154 Empty,
155 Full,
156}
157
158pub enum PromiseSetMode {
159 All,
160 Race,
161 Timeout(Duration),
162}
163pub struct PromiseSetWait {
164 pub promises: Vec<PromiseId>,
165 pub mode: PromiseSetMode,
166}
167
168#[derive(Clone)]
176pub struct ResourceGateHandle {
177 id: ResourceGateId,
178 closed: Rc<Cell<bool>>,
179 closer: Rc<ResourceGateCloser>,
180}
181
182type ResourceGateCloser = dyn Fn(ResourceGateId) -> Result<bool, ResourceGateCloseError> + 'static;
183
184impl ResourceGateHandle {
185 #[doc(hidden)]
188 pub fn new(id: ResourceGateId, closer: Rc<ResourceGateCloser>) -> Self {
189 Self {
190 id,
191 closed: Rc::new(Cell::new(false)),
192 closer,
193 }
194 }
195
196 pub fn id(&self) -> ResourceGateId {
197 self.id
198 }
199
200 pub fn close(&self) -> Result<bool, ResourceGateCloseError> {
204 if self.closed.get() {
205 return Ok(false);
206 }
207 let removed = (self.closer)(self.id)?;
208 self.closed.set(true);
209 Ok(removed)
210 }
211}
212
213impl fmt::Debug for ResourceGateHandle {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 f.debug_struct("ResourceGateHandle")
216 .field("id", &self.id)
217 .field("closed", &self.closed.get())
218 .finish_non_exhaustive()
219 }
220}
221
222impl Trace for ResourceGateHandle {
223 fn trace(&self, _sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
224 true
225 }
226}
227
228#[derive(Clone, Debug, Eq, PartialEq)]
229pub enum ResourceGateCloseError {
230 RuntimeUnavailable,
231 RuntimeBusy,
232 WrongRuntime,
233}
234
235impl fmt::Display for ResourceGateCloseError {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 match self {
238 Self::RuntimeUnavailable => f.write_str("resource gate runtime is no longer available"),
239 Self::RuntimeBusy => f.write_str("resource gate runtime is already mutably borrowed"),
240 Self::WrongRuntime => f.write_str("resource gate belongs to a different runtime"),
241 }
242 }
243}
244
245impl std::error::Error for ResourceGateCloseError {}
246
247#[derive(Clone, Debug)]
248pub enum RuntimeResponse {
249 Promise(PromiseId),
250 Channel(ChannelId),
251 ResourceGate(ResourceGateHandle),
252 Value(Value),
253 Cancelled(bool),
254 Settlement(Option<Rc<TaskSettlement>>),
255 Settlements(Vec<Rc<TaskSettlement>>),
256 Receive(ChannelReceive),
257 Send(ChannelSend),
258}
259
260#[derive(Clone, Copy, Debug, Eq, PartialEq)]
261pub enum ChannelSend {
262 Sent,
263 Closed,
264}
265
266#[derive(Clone, Debug)]
267pub enum ChannelReceive {
268 Received(Value),
269 Empty,
270 Closed,
271}
272
273pub struct NativeCall {
274 pub callable: Value,
275 pub args: Vec<Value>,
276 pub continuation: Box<dyn NativeContinuation>,
277}
278
279pub fn multimethod_call(
287 multimethod: Value,
288 args: Vec<Value>,
289 continuation: Box<dyn NativeContinuation>,
290) -> Result<NativeCall, SemaError> {
291 let dispatch_fn = multimethod
292 .as_multimethod_rc()
293 .ok_or_else(|| SemaError::type_error("multimethod", multimethod.type_name()))?
294 .dispatch_fn
295 .clone();
296 Ok(NativeCall {
297 callable: dispatch_fn,
298 args: args.clone(),
299 continuation: Box::new(MultimethodDispatchContinuation {
300 multimethod,
301 args,
302 continuation,
303 }),
304 })
305}
306
307struct MultimethodDispatchContinuation {
308 multimethod: Value,
309 args: Vec<Value>,
310 continuation: Box<dyn NativeContinuation>,
311}
312
313impl Trace for MultimethodDispatchContinuation {
314 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
315 sink(GcEdge::Value(&self.multimethod));
316 for arg in &self.args {
317 sink(GcEdge::Value(arg));
318 }
319 self.continuation.trace(sink)
320 }
321}
322
323impl NativeContinuation for MultimethodDispatchContinuation {
324 fn resume(
325 self: Box<Self>,
326 _context: &mut NativeCallContext<'_>,
327 input: ResumeInput,
328 ) -> NativeResult {
329 let dispatch_value = match input {
330 ResumeInput::Returned(value) => value,
331 ResumeInput::Failed(error) => return Err(error),
332 ResumeInput::Cancelled(reason) => {
333 return Err(SemaError::eval(format!(
334 "multimethod dispatch was cancelled ({reason:?})"
335 )))
336 }
337 ResumeInput::Runtime(_) => {
338 return Err(SemaError::eval(
339 "multimethod dispatch received an unexpected runtime response",
340 ))
341 }
342 };
343 let multimethod = self.multimethod.as_multimethod_rc().ok_or_else(|| {
344 SemaError::eval("internal error: multimethod dispatch state lost its callable")
345 })?;
346 let handler = crate::select_multimethod_handler(&multimethod, &dispatch_value)?;
347 Ok(NativeOutcome::Call(NativeCall {
348 callable: handler,
349 args: self.args,
350 continuation: self.continuation,
351 }))
352 }
353}
354
355pub struct NativeSuspend {
356 pub wait: WaitKind,
357 pub continuation: Box<dyn NativeContinuation>,
358}
359
360pub enum WaitKind {
361 Timer(Duration),
362 Promise(PromiseId),
363 PromiseSet(PromiseSetWait),
364 Channel(ChannelWait),
365 External(Box<PreparedExternalOperation>),
366 ResourceSlot(ResourceGateId),
370}
371
372pub enum ChannelWait {
373 Send { channel: ChannelId, value: Value },
374 Receive { channel: ChannelId },
375}
376
377pub enum ResumeInput {
378 Returned(Value),
379 Failed(SemaError),
380 Cancelled(CancelReason),
381 Runtime(RuntimeResponse),
382}
383
384pub trait NativeContinuation: Trace {
385 fn resume(
386 self: Box<Self>,
387 context: &mut NativeCallContext<'_>,
388 input: ResumeInput,
389 ) -> NativeResult;
390
391 fn is_trivial_spawn_handle(&self) -> bool {
400 false
401 }
402}
403
404fn trace_error(error: &SemaError, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
405 match error {
406 SemaError::UserException(value) | SemaError::Condition(value) => {
407 sink(GcEdge::Value(value));
408 true
409 }
410 SemaError::WithTrace { inner, .. } | SemaError::WithContext { inner, .. } => {
411 trace_error(inner, sink)
412 }
413 _ => true,
414 }
415}
416
417impl Trace for NativeOutcome {
418 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
419 match self {
420 Self::Return(value) => {
421 sink(GcEdge::Value(value));
422 true
423 }
424 Self::Call(call) => call.trace(sink),
425 Self::Suspend(suspend) => suspend.trace(sink),
426 Self::Runtime(request) => request.trace(sink),
427 }
428 }
429}
430
431impl Trace for RuntimeRequest {
432 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
433 match self {
434 Self::Spawn {
435 callable,
436 continuation,
437 } => {
438 sink(GcEdge::Value(callable));
439 continuation.trace(sink)
440 }
441 Self::CreateSettledPromise {
442 outcome,
443 continuation,
444 } => outcome.trace(sink) && continuation.trace(sink),
445 Self::CancelPromise { continuation, .. }
446 | Self::CreateChannel { continuation, .. }
447 | Self::ChannelOp { continuation, .. }
448 | Self::InspectPromise { continuation, .. }
449 | Self::PromiseSetWait { continuation, .. }
450 | Self::CreateResourceGate { continuation }
451 | Self::ReleaseResourceGate { continuation, .. }
452 | Self::CloseResourceGate { continuation, .. } => continuation.trace(sink),
453 Self::OriginBarrier { continuation } => continuation.trace(sink),
454 }
455 }
456}
457
458impl Trace for RuntimeResponse {
459 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
460 match self {
461 Self::Value(value) => sink(GcEdge::Value(value)),
462 Self::Receive(ChannelReceive::Received(value)) => sink(GcEdge::Value(value)),
463 Self::Settlement(Some(settlement)) => return settlement.trace(sink),
464 Self::Settlements(settlements) => {
465 return settlements.iter().all(|settlement| settlement.trace(sink));
466 }
467 _ => {}
468 }
469 true
470 }
471}
472
473impl Trace for NativeCall {
474 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
475 sink(GcEdge::Value(&self.callable));
476 for arg in &self.args {
477 sink(GcEdge::Value(arg));
478 }
479 self.continuation.trace(sink)
480 }
481}
482
483impl Trace for NativeSuspend {
484 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
485 self.wait.trace(sink) && self.continuation.trace(sink)
486 }
487}
488
489impl Trace for WaitKind {
490 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
491 match self {
492 Self::Timer(_) | Self::Promise(_) | Self::PromiseSet(_) | Self::ResourceSlot(_) => true,
493 Self::Channel(wait) => wait.trace(sink),
494 Self::External(operation) => operation.trace(sink),
495 }
496 }
497}
498
499impl Trace for ChannelWait {
500 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
501 match self {
502 Self::Send { value, .. } => sink(GcEdge::Value(value)),
503 Self::Receive { .. } => {}
504 }
505 true
506 }
507}
508
509impl Trace for ResumeInput {
510 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
511 match self {
512 Self::Returned(value) => {
513 sink(GcEdge::Value(value));
514 true
515 }
516 Self::Failed(error) => trace_error(error, sink),
517 Self::Cancelled(_) => true,
518 Self::Runtime(response) => response.trace(sink),
519 }
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use std::cell::{Cell, RefCell};
526 use std::collections::BTreeMap;
527 use std::rc::Rc;
528 use std::time::Duration;
529
530 use crate::cycle::GcEdge;
531 use crate::{EvalContext, NativeFn, SemaError, Value};
532
533 use super::*;
534 use crate::runtime::{ChannelId, RuntimeId, RuntimeScopedIdCounter, Trace};
535
536 struct Continuation {
537 edge: Value,
538 seen: Rc<RefCell<Vec<&'static str>>>,
539 }
540
541 impl Trace for Continuation {
542 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
543 sink(GcEdge::Value(&self.edge));
544 true
545 }
546 }
547
548 impl NativeContinuation for Continuation {
549 fn resume(
550 self: Box<Self>,
551 _context: &mut NativeCallContext<'_>,
552 input: ResumeInput,
553 ) -> NativeResult {
554 self.seen.borrow_mut().push(match input {
555 ResumeInput::Returned(_) => "returned",
556 ResumeInput::Failed(_) => "failed",
557 ResumeInput::Cancelled(_) => "cancelled",
558 ResumeInput::Runtime(_) => "runtime",
559 });
560 Ok(NativeOutcome::Return(self.edge))
561 }
562 }
563
564 fn channel() -> ChannelId {
565 let runtime = RuntimeId::allocate().unwrap();
566 RuntimeScopedIdCounter::new(runtime).allocate().unwrap()
567 }
568
569 fn promise() -> PromiseId {
570 let runtime = RuntimeId::allocate().unwrap();
571 RuntimeScopedIdCounter::new(runtime).allocate().unwrap()
572 }
573
574 fn resource_gate() -> ResourceGateId {
575 let runtime = RuntimeId::allocate().unwrap();
576 RuntimeScopedIdCounter::new(runtime).allocate().unwrap()
577 }
578
579 fn edge_count(trace: &impl Trace) -> usize {
580 let mut count = 0;
581 assert!(trace.trace(&mut |_| count += 1));
582 count
583 }
584
585 fn test_multimethod() -> Value {
586 let mut methods = BTreeMap::new();
587 methods.insert(Value::keyword("selected"), Value::string("handler"));
588 Value::multimethod(crate::MultiMethod {
589 name: crate::intern("test/structural-multimethod"),
590 dispatch_fn: Value::string("dispatch"),
591 methods: RefCell::new(methods),
592 default: RefCell::new(None),
593 })
594 }
595
596 #[test]
597 fn resource_gate_handle_is_trace_trivial_and_closes_once_across_clones() {
598 let gate = resource_gate();
599 let calls = Rc::new(Cell::new(0));
600 let calls_for_close = Rc::clone(&calls);
601 let handle = ResourceGateHandle::new(
602 gate,
603 Rc::new(move |_| {
604 calls_for_close.set(calls_for_close.get() + 1);
605 Ok(true)
606 }),
607 );
608 let clone = handle.clone();
609
610 assert_eq!(handle.id(), gate);
611 assert_eq!(edge_count(&handle), 0);
612 assert_eq!(handle.close(), Ok(true));
613 assert_eq!(clone.close(), Ok(false));
614 assert_eq!(calls.get(), 1, "the underlying closer runs exactly once");
615 }
616
617 #[test]
618 fn resource_gate_handle_remains_retryable_after_coordination_failure() {
619 let calls = Rc::new(Cell::new(0));
620 let calls_for_close = Rc::clone(&calls);
621 let handle = ResourceGateHandle::new(
622 resource_gate(),
623 Rc::new(move |_| {
624 calls_for_close.set(calls_for_close.get() + 1);
625 if calls_for_close.get() == 1 {
626 Err(ResourceGateCloseError::RuntimeBusy)
627 } else {
628 Ok(true)
629 }
630 }),
631 );
632
633 assert_eq!(handle.close(), Err(ResourceGateCloseError::RuntimeBusy));
634 assert_eq!(handle.close(), Ok(true));
635 assert_eq!(handle.close(), Ok(false));
636 assert_eq!(calls.get(), 2);
637 }
638
639 #[test]
640 fn dropping_resource_gate_handle_clones_does_not_close_the_gate() {
641 let calls = Rc::new(Cell::new(0));
642 let calls_for_close = Rc::clone(&calls);
643 let handle = ResourceGateHandle::new(
644 resource_gate(),
645 Rc::new(move |_| {
646 calls_for_close.set(calls_for_close.get() + 1);
647 Ok(true)
648 }),
649 );
650
651 drop(handle.clone());
652 assert_eq!(
653 calls.get(),
654 0,
655 "temporary capability clones are inert on drop"
656 );
657 assert_eq!(handle.close(), Ok(true));
658 assert_eq!(calls.get(), 1);
659 }
660
661 #[test]
662 fn protocol_shapes_and_structural_trace_multiplicity() {
663 let value = Value::string("same");
664 assert_eq!(edge_count(&NativeOutcome::Return(value.clone())), 1);
665
666 let call = NativeCall {
667 callable: value.clone(),
668 args: vec![value.clone(), value.clone()],
669 continuation: Box::new(Continuation {
670 edge: value.clone(),
671 seen: Rc::default(),
672 }),
673 };
674 assert_eq!(edge_count(&call), 4);
675 assert_eq!(edge_count(&NativeOutcome::Call(call)), 4);
676
677 let send = ChannelWait::Send {
678 channel: channel(),
679 value: value.clone(),
680 };
681 assert_eq!(edge_count(&send), 1);
682 assert_eq!(edge_count(&ChannelWait::Receive { channel: channel() }), 0);
683 assert_eq!(edge_count(&WaitKind::Timer(Duration::from_millis(1))), 0);
684 assert_eq!(edge_count(&WaitKind::Promise(promise())), 0);
685 assert_eq!(edge_count(&WaitKind::Channel(send)), 1);
686
687 let suspend = NativeSuspend {
688 wait: WaitKind::Channel(ChannelWait::Send {
689 channel: channel(),
690 value: value.clone(),
691 }),
692 continuation: Box::new(Continuation {
693 edge: value.clone(),
694 seen: Rc::default(),
695 }),
696 };
697 assert_eq!(edge_count(&suspend), 2);
698 assert_eq!(edge_count(&NativeOutcome::Suspend(suspend)), 2);
699 assert_eq!(edge_count(&ResumeInput::Returned(value.clone())), 1);
700 assert_eq!(
701 edge_count(&ResumeInput::Failed(SemaError::Condition(value))),
702 1
703 );
704 assert_eq!(
705 edge_count(&ResumeInput::Cancelled(CancelReason::Explicit)),
706 0
707 );
708 }
709
710 #[test]
711 fn multimethod_call_traces_both_structural_stages() {
712 let retained = Value::string("outer-retained");
713 let seen = Rc::new(RefCell::new(Vec::new()));
714 let dispatch = multimethod_call(
715 test_multimethod(),
716 vec![Value::int(7)],
717 Box::new(Continuation {
718 edge: retained,
719 seen,
720 }),
721 )
722 .expect("multimethod call");
723
724 assert_eq!(
725 edge_count(&dispatch),
726 5,
727 "dispatch callable + arg + retained multimethod + retained arg + outer continuation"
728 );
729
730 let eval_context = EvalContext::new();
731 let task_context = TaskContextHandle::default();
732 let mut context = NativeCallContext {
733 hof_host: None,
734 eval_context: &eval_context,
735 task_context,
736 call_env: None,
737 cancellation: CancellationView::default(),
738 };
739 let outcome = dispatch
740 .continuation
741 .resume(
742 &mut context,
743 ResumeInput::Returned(Value::keyword("selected")),
744 )
745 .expect("dispatch selects handler");
746 let NativeOutcome::Call(handler) = outcome else {
747 panic!("dispatch continuation must emit the handler call")
748 };
749 assert_eq!(handler.callable, Value::string("handler"));
750 assert_eq!(handler.args, vec![Value::int(7)]);
751 assert_eq!(
752 edge_count(&handler),
753 3,
754 "selected handler + arg + outer continuation"
755 );
756 }
757
758 #[test]
759 fn multimethod_dispatch_propagates_failure_cancellation_and_protocol_error() {
760 fn dispatch_continuation() -> Box<dyn NativeContinuation> {
761 multimethod_call(
762 test_multimethod(),
763 vec![Value::int(7)],
764 Box::new(Continuation {
765 edge: Value::nil(),
766 seen: Rc::default(),
767 }),
768 )
769 .expect("multimethod call")
770 .continuation
771 }
772
773 let eval_context = EvalContext::new();
774 let task_context = TaskContextHandle::default();
775 let mut context = NativeCallContext {
776 hof_host: None,
777 eval_context: &eval_context,
778 task_context,
779 call_env: None,
780 cancellation: CancellationView::default(),
781 };
782
783 let failure = dispatch_continuation()
784 .resume(
785 &mut context,
786 ResumeInput::Failed(SemaError::eval("dispatch failed")),
787 )
788 .err()
789 .expect("failure propagates");
790 assert!(failure.to_string().contains("dispatch failed"));
791
792 let cancelled = dispatch_continuation()
793 .resume(&mut context, ResumeInput::Cancelled(CancelReason::Explicit))
794 .err()
795 .expect("cancellation propagates");
796 assert!(cancelled.to_string().contains("cancelled"));
797
798 let protocol = dispatch_continuation()
799 .resume(
800 &mut context,
801 ResumeInput::Runtime(RuntimeResponse::Value(Value::nil())),
802 )
803 .err()
804 .expect("unexpected runtime response fails");
805 assert!(protocol.to_string().contains("unexpected runtime response"));
806 }
807
808 #[test]
809 fn tracing_keeps_partial_output_when_continuation_fails() {
810 struct BorrowingContinuation {
811 first: Value,
812 second: Rc<RefCell<Value>>,
813 }
814 impl Trace for BorrowingContinuation {
815 fn trace(&self, sink: &mut dyn FnMut(GcEdge<'_>)) -> bool {
816 sink(GcEdge::Value(&self.first));
817 match self.second.try_borrow() {
818 Ok(second) => {
819 sink(GcEdge::Value(&second));
820 true
821 }
822 Err(_) => false,
823 }
824 }
825 }
826 impl NativeContinuation for BorrowingContinuation {
827 fn resume(
828 self: Box<Self>,
829 _context: &mut NativeCallContext<'_>,
830 _input: ResumeInput,
831 ) -> NativeResult {
832 Ok(NativeOutcome::Return(self.first))
833 }
834 }
835
836 let continuation = BorrowingContinuation {
837 first: Value::NIL,
838 second: Rc::new(RefCell::new(Value::NIL)),
839 };
840 let second = Rc::clone(&continuation.second);
841 let borrow = second.borrow_mut();
842 let call = NativeCall {
843 callable: Value::NIL,
844 args: vec![Value::NIL],
845 continuation: Box::new(continuation),
846 };
847 let mut emitted = 0;
848 assert!(!call.trace(&mut |_| emitted += 1));
849 assert_eq!(emitted, 3);
850 drop(borrow);
851 }
852
853 #[test]
854 fn continuation_is_consumed_for_each_resume_input() {
855 let seen = Rc::new(RefCell::new(Vec::new()));
856 let eval_context = EvalContext::new();
857 let task_context = TaskContextHandle::default();
858 let mut context = NativeCallContext {
859 hof_host: None,
860 eval_context: &eval_context,
861 task_context,
862 call_env: None,
863 cancellation: CancellationView::default(),
864 };
865 for input in [
866 ResumeInput::Returned(Value::NIL),
867 ResumeInput::Failed(SemaError::eval("failed")),
868 ResumeInput::Cancelled(CancelReason::Explicit),
869 ] {
870 Box::new(Continuation {
871 edge: Value::NIL,
872 seen: Rc::clone(&seen),
873 })
874 .resume(&mut context, input)
875 .unwrap();
876 }
877 assert_eq!(&*seen.borrow(), &["returned", "failed", "cancelled"]);
878 }
879
880 #[test]
881 fn legacy_runtime_fallback_uses_embedded_eval_context() {
882 let embedded = EvalContext::new();
883 let observed = Rc::new(Cell::new(std::ptr::null::<EvalContext>()));
884 let observed_by_native = Rc::clone(&observed);
885 let native = NativeFn::with_ctx("context-identity", move |context, _| {
886 observed_by_native.set(context);
887 Ok(Value::NIL)
888 });
889 let task_context = TaskContextHandle::default();
890 let mut runtime_context = NativeCallContext {
891 hof_host: None,
892 eval_context: &embedded,
893 task_context,
894 call_env: None,
895 cancellation: CancellationView::default(),
896 };
897
898 assert!(matches!(
899 native.invoke_runtime(&mut runtime_context, &[]),
900 Ok(NativeOutcome::Return(value)) if value.is_nil()
901 ));
902 assert_eq!(observed.get(), &embedded as *const EvalContext);
903 }
904
905 #[test]
906 fn native_fn_dual_abi_preserves_legacy_and_runtime_paths() {
907 let eval = EvalContext::new();
908 let seen_eval = Rc::new(Cell::new(std::ptr::null::<EvalContext>()));
909 let task_context = TaskContextHandle::default();
910 let mut runtime = NativeCallContext {
911 hof_host: None,
912 eval_context: &eval,
913 task_context,
914 call_env: None,
915 cancellation: CancellationView::default(),
916 };
917 let legacy = NativeFn::simple("legacy", |_| Ok(Value::int(7)));
918 assert_eq!((legacy.func)(&eval, &[]).unwrap(), Value::int(7));
919 assert!(
920 matches!(legacy.invoke_runtime(&mut runtime, &[]), Ok(NativeOutcome::Return(v)) if v == Value::int(7))
921 );
922 let seen_eval_from_callback = Rc::clone(&seen_eval);
923 let with_ctx = NativeFn::with_ctx("with-ctx", move |ctx, _| {
924 seen_eval_from_callback.set(ctx);
925 Ok(Value::int(6))
926 });
927 assert!(
928 matches!(with_ctx.invoke_runtime(&mut runtime, &[]), Ok(NativeOutcome::Return(v)) if v == Value::int(6))
929 );
930 assert_eq!(seen_eval.get(), &eval as *const EvalContext);
931 let payload: Rc<dyn std::any::Any> = Rc::new(Value::int(5));
932 let with_payload = NativeFn::with_payload("with-payload", Rc::clone(&payload), |_, _| {
933 Ok(Value::int(5))
934 });
935 assert!(Rc::ptr_eq(with_payload.payload.as_ref().unwrap(), &payload));
936 assert!(
937 matches!(with_payload.invoke_runtime(&mut runtime, &[]), Ok(NativeOutcome::Return(v)) if v == Value::int(5))
938 );
939
940 let result =
941 NativeFn::simple_result("runtime", |_| Ok(NativeOutcome::Return(Value::int(8))));
942 assert!(
943 matches!(result.invoke_runtime(&mut runtime, &[]), Ok(NativeOutcome::Return(v)) if v == Value::int(8))
944 );
945 assert!((result.func)(&eval, &[])
946 .unwrap_err()
947 .to_string()
948 .contains("runtime"));
949
950 let contextual = NativeFn::with_context_result("contextual", |runtime, args| {
951 assert!(!runtime.cancellation.is_requested());
952 let _task_context = runtime.task_context.borrow_mut();
953 Ok(NativeOutcome::Return(args[0].clone()))
954 });
955 assert!(
956 matches!(contextual.invoke_runtime(&mut runtime, &[Value::int(9)]), Ok(NativeOutcome::Return(v)) if v == Value::int(9))
957 );
958 assert!((contextual.func)(&eval, &[])
959 .unwrap_err()
960 .to_string()
961 .contains("contextual"));
962 }
963
964 #[test]
965 fn payload_runtime_native_uses_one_typed_payload_owner() {
966 struct Payload {
967 value: RefCell<Value>,
968 }
969
970 fn invoke(
971 payload: &Payload,
972 runtime: &mut NativeCallContext<'_>,
973 args: &[Value],
974 ) -> NativeResult {
975 assert!(!runtime.cancellation.is_requested());
976 let previous = payload.value.replace(args[0].clone());
977 Ok(NativeOutcome::Return(previous))
978 }
979
980 let payload = Rc::new(Payload {
981 value: RefCell::new(Value::int(10)),
982 });
983 let native = NativeFn::with_payload_result("payload-runtime", Rc::clone(&payload), invoke);
984 assert_eq!(Rc::strong_count(&payload), 2, "caller plus payload field");
985 assert!(native
986 .payload
987 .as_ref()
988 .unwrap()
989 .downcast_ref::<Payload>()
990 .is_some());
991
992 let eval = EvalContext::new();
993 let task_context = TaskContextHandle::default();
994 let mut runtime = NativeCallContext {
995 hof_host: None,
996 eval_context: &eval,
997 task_context,
998 call_env: None,
999 cancellation: CancellationView::default(),
1000 };
1001 assert!(matches!(
1002 native.invoke_runtime(&mut runtime, &[Value::int(11)]),
1003 Ok(NativeOutcome::Return(value)) if value == Value::int(10)
1004 ));
1005 assert_eq!(*payload.value.borrow(), Value::int(11));
1006 assert!((native.func)(&eval, &[])
1007 .unwrap_err()
1008 .to_string()
1009 .contains("payload-runtime"));
1010 }
1011}