Skip to main content

sema_core/runtime/
native.rs

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    /// Synchronous-HOF capability, installed only by the VM's in-quantum native
41    /// dispatch. `None` everywhere else (drive-level dispatch, restricted runs,
42    /// host adapters) — a HOF then takes its cooperative `NativeOutcome::Call`
43    /// path unchanged.
44    pub hof_host: Option<&'a dyn SyncHofHost>,
45}
46
47/// Host capability for running a cooperative HOF's callback chain synchronously
48/// inside the active runtime quantum. Implemented by the VM and offered to
49/// natives through [`NativeCallContext::hof_host`].
50///
51/// [`run_sync_hof`](SyncHofHost::run_sync_hof) declines (`None`) unless
52/// `callable` is a VM closure whose call graph provably cannot suspend — the
53/// caller must then fall back to its cooperative path. When it accepts, it runs
54/// `driver` with a live [`SyncCallbackSession`] and returns the driver's result
55/// as the native's outcome.
56pub 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
64/// One accepted synchronous HOF run: per-element direct calls of the proven
65/// non-suspending callback on a scratch VM, no scheduler involvement.
66pub trait SyncCallbackSession {
67    /// Call the callback once. `args` is an owned buffer the caller will not
68    /// reuse — values are moved into the callee frame (nils left behind), so a
69    /// uniquely-owned fold accumulator stays uniquely owned across the call.
70    fn call_owned(&mut self, args: &mut [Value]) -> Result<Value, SemaError>;
71
72    /// True once this drive quantum's instruction budget is exhausted; the
73    /// driver must then hand its remaining elements back to the cooperative
74    /// path (typically as the HOF's ordinary mid-chain continuation).
75    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    /// Allocate a fresh [`ResourceGateId`] — a per-handle mutual-exclusion slot
121    /// with a FIFO waiter queue. A checkout-style stdlib module (sqlite, kv,
122    /// proc, pty, serial, stream) creates one gate per resource handle when the
123    /// handle is opened, then acquires it via [`WaitKind::ResourceSlot`] before
124    /// each offloaded op and releases it via [`RuntimeRequest::ReleaseResourceGate`]
125    /// when the op completes. The continuation receives [`RuntimeResponse::ResourceGate`].
126    CreateResourceGate {
127        continuation: Box<dyn NativeContinuation>,
128    },
129    /// Release ownership of a previously-acquired resource gate, waking the FIFO
130    /// head waiter (if any) so exactly one queued acquirer proceeds. The
131    /// continuation resumes with `RuntimeResponse::Value(nil)`.
132    ReleaseResourceGate {
133        gate: ResourceGateId,
134        continuation: Box<dyn NativeContinuation>,
135    },
136    /// Close a resource gate: fail every parked waiter with a structured
137    /// "gate closed" error and drop the gate record. Used when a handle is
138    /// closed/tombstoned so queued acquirers fail fast rather than hang.
139    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/// A VM-thread capability for one runtime resource gate.
169///
170/// Native checkout paths use [`ResourceGateHandle::id`] with the ordinary
171/// `WaitKind::ResourceSlot` / `RuntimeRequest` protocol. The close capability
172/// exists for lifecycle edges that cannot first store the id (allocation
173/// delivery cancelled) and host-only cleanup that runs outside a native
174/// continuation. Clones share the close-once state.
175#[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    /// Construct a gate capability around the owning runtime's weak closer.
186    /// Runtime implementations are the intended callers.
187    #[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    /// Close the gate through its owning runtime. Returns `Ok(true)` when this
201    /// call removed the live gate, `Ok(false)` when it was already closed, and
202    /// leaves the capability retryable when runtime coordination fails.
203    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
279/// Build the first stage of a structural multimethod call.
280///
281/// The returned call invokes the dispatch function. Its continuation retains
282/// the multimethod, original arguments, and caller continuation, selects a
283/// handler from the returned dispatch value, and emits a second
284/// [`NativeOutcome::Call`] for that handler. No evaluator callback is performed
285/// inside the active runtime quantum.
286pub 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    /// Park until this task owns `gate`'s exclusive slot. Resumes with
367    /// `RuntimeResponse::Value(nil)` once the slot is granted (immediately if
368    /// the gate is free, otherwise FIFO-behind any earlier acquirers).
369    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    /// True ONLY for a [`RuntimeRequest::Spawn`] continuation whose entire
392    /// behavior is mapping `RuntimeResponse::Promise(id)` to
393    /// `Value::async_promise_id(id)` — `async/spawn`'s own default. The spawn
394    /// dispatcher's parked-VM fast path inlines exactly that mapping and skips
395    /// invoking the continuation; a continuation that leaves this `false` is
396    /// routed through the general pending-stage path so its logic actually
397    /// runs. Do not return `true` from a continuation that does anything else:
398    /// the fast path would silently discard that behavior.
399    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}