Skip to main content

luna_core/vm/
async_drive.rs

1//! Cooperative-yield core for `Vm::eval_async`.
2//!
3//! This module implements:
4//!
5//! - `DispatchOutcome` — terminal / cooperative-yield enum.
6//! - `Vm::drive_one` — runs the dispatcher until completion / error /
7//!   `BudgetExhausted`. Layers on `Vm::call_value` for the bootstrap
8//!   poll and on `Vm::exec_with_async` for resume polls.
9//! - [`EvalFuture`] — `!Send` `std::future::Future` that owns the
10//!   `&mut Vm` borrow and surfaces the poll loop.
11//! - [`Vm::eval_async`] / [`Vm::eval_async_chunk`] — public entry
12//!   points; convenience for embedders wanting `tokio` / `async-std`
13//!   integration.
14//!
15//! Async mode auto-disables JIT for the future's lifetime and
16//! restores the prior setting on terminal poll.
17//!
18//! ```
19//! use luna_core::vm::Vm;
20//! use luna_core::version::LuaVersion;
21//! use std::future::Future;
22//! use std::pin::Pin;
23//! use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
24//!
25//! // 20-line hand-rolled block_on (no tokio dep).
26//! fn block_on<F: Future>(mut fut: F) -> F::Output {
27//!     fn raw_waker() -> RawWaker {
28//!         fn noop(_: *const ()) {}
29//!         fn clone(_: *const ()) -> RawWaker { raw_waker() }
30//!         static VT: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
31//!         RawWaker::new(std::ptr::null(), &VT)
32//!     }
33//!     let waker = unsafe { Waker::from_raw(raw_waker()) };
34//!     let mut cx = Context::from_waker(&waker);
35//!     let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
36//!     loop {
37//!         match fut.as_mut().poll(&mut cx) {
38//!             Poll::Ready(v) => return v,
39//!             Poll::Pending => continue,
40//!         }
41//!     }
42//! }
43//!
44//! let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
45//! let r = block_on(vm.eval_async("return 1 + 2")).unwrap();
46//! assert_eq!(r.len(), 1);
47//! ```
48
49use crate::runtime::Value;
50use crate::vm::error::LuaError;
51use crate::vm::exec::Vm;
52use std::future::Future;
53use std::pin::Pin;
54use std::task::{Context, Poll};
55
56/// Async-native function ABI. Returns a
57/// `Pin<Box<dyn Future>>` that resolves to the return-value count
58/// (same convention as sync [`crate::runtime::value::NativeFn`]: write
59/// results into the caller's slot via the borrowed `Vm`, then yield
60/// the count back).
61///
62/// # Safety contract
63///
64/// The first parameter is `*mut Vm` rather than `&mut Vm` because the
65/// returned `Pin<Box<dyn Future>>` is `'static` (the trait object
66/// erases lifetimes) and we cannot tie it to the caller's borrow
67/// without `for<'vm>` HRTBs that the trait system rejects on `dyn`
68/// futures. Implementors must reborrow inside the future:
69///
70/// ```ignore
71/// fn my_async(
72///     vm: *mut Vm,
73///     func_slot: u32,
74///     nargs: u32,
75/// ) -> Pin<Box<dyn Future<Output = Result<u32, LuaError>>>> {
76///     Box::pin(async move {
77///         // SAFETY: the dispatcher is suspended and EvalFuture
78///         // holds the unique &mut Vm borrow for the future's
79///         // entire lifetime; no concurrent access can occur.
80///         let vm = unsafe { &mut *vm };
81///         // ... read args from vm.stack[func_slot+1..], do async
82///         //     work (e.g. `sleep(...).await`), write results back
83///         //     to vm.stack[func_slot..], return their count ...
84///         Ok(0)
85///     })
86/// }
87/// ```
88///
89/// The `Vm` is exclusively owned by the active [`EvalFuture`] for the
90/// suspension's full lifetime (the dispatcher is paused; the host's
91/// executor is the only driver). This makes the `unsafe { &mut *vm }`
92/// reborrow sound provided the future doesn't leak the borrow past
93/// its own `await` boundaries.
94///
95/// The native is invoked exactly once per Lua call site. The future
96/// is polled by [`EvalFuture::poll`]; on `Poll::Ready(Ok(n))` the
97/// dispatcher resumes, treats slots `[func_slot, func_slot+n)` as the
98/// return list, and continues. On `Poll::Ready(Err(e))` the error
99/// propagates as if a sync native had returned it.
100pub type AsyncNativeFn =
101    fn(*mut Vm, func_slot: u32, nargs: u32) -> Pin<Box<dyn Future<Output = Result<u32, LuaError>>>>;
102
103/// Outcome of a single dispatcher slice driven by
104/// [`Vm::drive_one`]. The `AsyncNativeAwaiting` variant is
105/// for async natives: the dispatcher suspends in-place, hands the
106/// returned future to [`EvalFuture::poll`], and resumes the same call
107/// site once the future resolves.
108pub(crate) enum DispatchOutcome {
109    /// The chunk returned cleanly; values are the Lua-side return list.
110    Complete(Vec<Value>),
111    /// A genuine runtime / syntax / type error (NOT a budget yield).
112    Error(LuaError),
113    /// The per-poll instruction quota was exhausted. The dispatcher's
114    /// call frames are intact; the next [`Vm::drive_one`] call (after
115    /// the host pumps the executor) resumes from the same point.
116    BudgetExhausted,
117    /// The dispatcher invoked an async-marked
118    /// native; the returned future is now under host drive. The Vm
119    /// preserves the in-flight call's `(func_slot, nargs, nresults)`
120    /// context in `pending_async_native_ctx` so that
121    /// [`Vm::commit_async_native_result`] can land the future's
122    /// eventual `Ok(nret)` back into the calling frame.
123    AsyncNativeAwaiting(Pin<Box<dyn Future<Output = Result<u32, LuaError>>>>),
124}
125
126impl Vm {
127    /// Allocate a `Value::Native` whose closure is
128    /// tagged as async (`NativeClosure.is_async = true`). The
129    /// underlying `NativeFn` pointer slot stores `f` transmuted from
130    /// [`AsyncNativeFn`] — same pointer width, no provenance loss —
131    /// and the marker bit is what tells the dispatcher to route it
132    /// through the cooperative-yield path.
133    ///
134    /// The returned `Value` can be installed under a Lua global via
135    /// [`Vm::set_global`], passed as a callback, stored in a table —
136    /// whatever a sync `vm.native(f)` value supports. Calling it from
137    /// a sync `Vm::eval` context raises `LuaError` ("async native
138    /// called in sync context"); only `Vm::eval_async` (or another
139    /// driver that sets `async_mode = true`) can drive it.
140    pub fn create_async_native(&mut self, f: AsyncNativeFn) -> Value {
141        // SAFETY: `AsyncNativeFn` and `NativeFn` are both Rust `fn`
142        // pointers and have identical size + alignment (single word).
143        // The `is_async` marker bit, set by `Heap::new_async_native`,
144        // is the discriminant the dispatcher reads before transmuting
145        // back to `AsyncNativeFn` at the call site; without the bit
146        // the pointer is never invoked.
147        let raw_fn: crate::runtime::value::NativeFn = unsafe { std::mem::transmute(f) };
148        Value::Native(self.heap.new_async_native(raw_fn, Box::new([])))
149    }
150
151    /// Convenience: install an async native under
152    /// `name` as a Lua global. Equivalent to
153    /// `vm.set_global(name, vm.create_async_native(f))`.
154    pub fn set_async_native(&mut self, name: &str, f: AsyncNativeFn) -> Result<(), LuaError> {
155        let v = self.create_async_native(f);
156        self.set_global(name, v)
157    }
158
159    /// Convenience entry: compile + run `src` as an
160    /// anonymous chunk via the cooperative-yield dispatcher. The
161    /// returned `EvalFuture` borrows `&mut self` for its full lifetime,
162    /// which (by `Vm: !Send`) keeps it pinned to a single OS thread.
163    ///
164    /// Holding two `EvalFuture`s on the same Vm is blocked by the
165    /// borrow checker (`&mut Vm` exclusivity). Holding a sync
166    /// `eval`/`call_value` call *while* an `EvalFuture` is in flight
167    /// is likewise blocked.
168    ///
169    /// The chunk source name in tracebacks is `"=eval"`. Use
170    /// [`Vm::eval_async_chunk`] to supply a custom name.
171    pub fn eval_async<'vm>(&'vm mut self, src: &str) -> EvalFuture<'vm> {
172        self.eval_async_chunk(src, "=eval")
173    }
174
175    /// Like [`Vm::eval_async`] but with a
176    /// user-supplied chunk name (appears in tracebacks).
177    pub fn eval_async_chunk<'vm>(&'vm mut self, src: &str, name: &str) -> EvalFuture<'vm> {
178        EvalFuture {
179            vm: self,
180            state: EvalState::Initial {
181                src: src.to_string(),
182                name: name.to_string(),
183            },
184            saved_jit_enabled: None,
185            saved_async_slice: None,
186        }
187    }
188
189    /// Set the per-poll opcode quota loaded into
190    /// `instr_budget` at the start of each [`EvalFuture`] poll slice.
191    /// Default 10_000 opcodes. Smaller = finer-grained cooperative
192    /// yield (lower per-task latency, more task-switch overhead);
193    /// larger = closer to sync throughput per slice.
194    pub fn set_async_slice(&mut self, n: i64) {
195        // i64::MAX silently caps at i64::MAX; non-positive values
196        // would loop indefinitely so clamp to 1 (a single opcode per
197        // slice — pathological but well-defined).
198        self.async_slice_size = n.max(1);
199    }
200
201    /// Current per-poll async slice size (default
202    /// 10_000).
203    pub fn async_slice(&self) -> i64 {
204        self.async_slice_size
205    }
206
207    /// Drive the dispatcher one slice. Used
208    /// internally by [`EvalFuture::poll`]. The `bootstrap` flag tells
209    /// the helper whether this is the first slice of a fresh chunk
210    /// (in which case `call_value` sets up the call frame) or a
211    /// resume (in which case the existing frames live in `self.frames`
212    /// and the helper just re-enters the dispatcher at the saved
213    /// `entry_depth`).
214    pub(crate) fn drive_one(
215        &mut self,
216        bootstrap: Option<Value>,
217        entry_depth: usize,
218    ) -> DispatchOutcome {
219        // Arm `async_mode` so the budget hot loop yields cooperatively
220        // instead of erroring. The future installs this once on the
221        // first poll and clears it on terminal poll; arming again here
222        // is idempotent.
223        self.async_mode = true;
224        // Arm a fresh slice quota. The previous slice exhausted to 0;
225        // `instr_budget` was set to `None` by the hot loop on
226        // exhaustion. Reload it for this slice.
227        self.instr_budget = Some(self.async_slice_size);
228
229        let raw = match bootstrap {
230            Some(closure_val) => {
231                // First slice — set up the call frame via the existing
232                // `call_value` path. This handles `c_depth`,
233                // `public_call_depth`, `clear_error_metadata`, and the
234                // `begin_call` push. On a synchronous completion (e.g.
235                // a chunk whose only op is `return`) the call
236                // finishes within `call_value` and we hit
237                // `Complete` immediately.
238                self.call_value(closure_val, &[])
239            }
240            None => {
241                // Resume slice — frames are intact from the prior
242                // `BudgetExhausted`. Walk the dispatcher again.
243                self.exec_with_async(entry_depth)
244            }
245        };
246
247        match raw {
248            Ok(values) => DispatchOutcome::Complete(values),
249            Err(e) => {
250                // Async-native suspension takes
251                // precedence: the future is the active work item, the
252                // sentinel Err is just transport. Check before
253                // `host_yield_pending` because both flags can in
254                // principle coexist (a budget exhaustion deferred by
255                // an in-flight async-native call) but the async-native
256                // future must be drained first.
257                if self.pending_async_native_fut.is_some() {
258                    let fut = self.pending_async_native_fut.take().expect("checked above");
259                    // ctx stays in place — `commit_async_native_result`
260                    // consumes it when the future resolves.
261                    DispatchOutcome::AsyncNativeAwaiting(fut)
262                } else if self.host_yield_pending {
263                    self.host_yield_pending = false;
264                    DispatchOutcome::BudgetExhausted
265                } else {
266                    DispatchOutcome::Error(e)
267                }
268            }
269        }
270    }
271
272    /// Land an async native's resolved return
273    /// count back into the calling frame's expected result slots.
274    /// Mirrors the sync-native tail of `call_at` (sans the
275    /// `running_natives` bookkeeping). Consumes
276    /// `Vm.pending_async_native_ctx`; subsequent `drive_one` calls
277    /// resume the dispatcher above this call site.
278    ///
279    /// Called by [`EvalFuture::poll`] after the awaited future
280    /// resolves to `Poll::Ready(Ok(nret))`.
281    pub(crate) fn commit_async_native_result(&mut self, nret: u32) -> Result<(), LuaError> {
282        let ctx = self
283            .pending_async_native_ctx
284            .take()
285            .expect("commit_async_native_result without a pending ctx");
286        self.finish_results(ctx.func_slot, nret, ctx.nresults);
287        // Fire the matching "return" hook for the
288        // async native, after results land in the call window and
289        // before the post-call GC checkpoint. Mirrors the sync
290        // native's `hook_return(true, nargs + 1, nret)` placement in
291        // `exec.rs`. The sync path widens its C-frame argument window
292        // around the hook so `debug.getlocal(2, ftransfer..)` reads
293        // the results; the async path doesn't push to
294        // `running_natives` (the future owned the borrow window
295        // across `.await`), so there's no `running_native_slots` to
296        // widen — `hook_ftransfer` / `hook_ntransfer` set by
297        // `hook_return` carry the same information for Rust hooks
298        // and for Lua hooks reading `debug.getinfo(.).ftransfer`.
299        let ftransfer = ctx.nargs + 1;
300        self.hook_return(true, ftransfer, nret)?;
301        // Same post-call GC checkpoint the sync path runs: the native
302        // may have allocated, and the live boundary is now the result
303        // window.
304        self.maybe_collect_garbage(self.top);
305        Ok(())
306    }
307}
308
309/// Host-driven cooperative-yield future. Borrows
310/// `&mut Vm` for its full lifetime; the borrow + `Vm: !Send` together
311/// make the future `!Send` (suits tokio `current_thread` /
312/// `LocalSet`, NOT multi-thread runtimes).
313///
314/// See module docs for a hand-rolled `block_on` usage example.
315pub struct EvalFuture<'vm> {
316    vm: &'vm mut Vm,
317    state: EvalState,
318    /// Saved `jit.enabled` snapshot from the first poll. JIT-compiled
319    /// traces don't honor `instr_budget` at every opcode, so a runaway
320    /// trace in async mode could starve other tokio tasks. The future
321    /// disables JIT for its duration and restores on terminal poll
322    /// (or on Drop).
323    saved_jit_enabled: Option<bool>,
324    /// Saved `async_slice_size`. The future doesn't mutate it; the
325    /// field lets an async-native path install per-future slice tweaks
326    /// without leaking them into sibling futures.
327    #[allow(dead_code)]
328    saved_async_slice: Option<i64>,
329}
330
331/// State machine driving an `EvalFuture`.
332///
333/// - `Initial` — pre-compile. The source string is owned so the
334///   future can outlive the caller's `&str`.
335/// - `Running` — bootstrap done; subsequent polls resume from
336///   `entry_depth`.
337/// - `Done` — terminal. Polling again panics (per `Future` contract:
338///   futures must not be polled after `Poll::Ready`).
339enum EvalState {
340    Initial {
341        src: String,
342        name: String,
343    },
344    Running {
345        entry_depth: usize,
346        /// `true` only on the very first slice — we still need to
347        /// invoke `call_value` to push the entry frame. After the
348        /// first `BudgetExhausted`, this flips to `false` and the
349        /// future resumes via `exec_with_async`.
350        first_slice: bool,
351        /// Cached for `bootstrap = Some(...)`. After bootstrap fires
352        /// once, the value is `None`.
353        closure: Option<Value>,
354    },
355    /// An async native is mid-await. The future is
356    /// owned here (rather than on the `Vm`) so an explicit `Drop` of
357    /// `EvalFuture` cancels the in-flight future cleanly. On the next
358    /// poll: if the future resolves to `Ok(nret)`, the EvalFuture
359    /// calls `Vm::commit_async_native_result(nret)` and falls back to
360    /// `EvalState::Running` to keep driving the dispatcher; on `Err`
361    /// the EvalFuture transitions to `Done` and surfaces the error.
362    AwaitingNative {
363        entry_depth: usize,
364        fut: Pin<Box<dyn Future<Output = Result<u32, LuaError>>>>,
365    },
366    Done,
367}
368
369impl<'vm> Future for EvalFuture<'vm> {
370    type Output = Result<Vec<Value>, LuaError>;
371
372    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
373        // `EvalFuture` holds no self-referential state — `vm` is a
374        // plain mutable borrow, `state` is owned by value. Safe to
375        // project out of the pin without `pin-project`.
376        let this = unsafe { self.as_mut().get_unchecked_mut() };
377
378        loop {
379            // ---- State transition: Initial → Running ----
380            if let EvalState::Initial { src, name } = &this.state {
381                // Stash JIT setting + disable for the duration (JIT
382                // traces don't honor instr_budget per opcode, so async
383                // mode + JIT could starve the executor).
384                if this.saved_jit_enabled.is_none() {
385                    this.saved_jit_enabled = Some(this.vm.jit_enabled());
386                    this.vm.set_jit_enabled(false);
387                }
388                // Compile. On syntax error we transition directly to
389                // Done with the error — no Lua frames were pushed,
390                // so the Vm is back at quiescent state.
391                let cl = match this.vm.load(src.as_bytes(), name.as_bytes()) {
392                    Ok(c) => c,
393                    Err(syntax) => {
394                        // Match `eval_chunk`'s syntax-error shaping
395                        // (error classification + source position).
396                        this.vm
397                            .set_error_kind(crate::vm::error::LuaErrorKind::Syntax);
398                        this.vm.set_error_source(name.clone(), syntax.line);
399                        let msg = format!("{}", syntax);
400                        let s = this.vm.intern_str(&msg);
401                        // Restore JIT + clean up before returning.
402                        if let Some(prev) = this.saved_jit_enabled.take() {
403                            this.vm.set_jit_enabled(prev);
404                        }
405                        this.vm.async_mode = false;
406                        this.vm.async_waker = None;
407                        this.state = EvalState::Done;
408                        return Poll::Ready(Err(LuaError(Value::Str(s))));
409                    }
410                };
411                // For the bootstrap slice, frames.len() is currently
412                // 0 (no prior calls on this Vm: enforced by `&mut
413                // Vm` exclusivity over the future's lifetime). The
414                // `call_value` path will push one Lua frame, so the
415                // saved `entry_depth` is 1. We capture it explicitly
416                // rather than reading `vm.frames.len()` post-call so
417                // resume after BudgetExhausted reuses the right
418                // depth.
419                let entry_depth = this.vm.frame_count().saturating_add(1);
420                this.state = EvalState::Running {
421                    entry_depth,
422                    first_slice: true,
423                    closure: Some(Value::Closure(cl)),
424                };
425                // Fall through to Running.
426            }
427
428            // ---- State: Running. Drive a slice. ----
429            match &mut this.state {
430                EvalState::Running {
431                    entry_depth,
432                    first_slice,
433                    closure,
434                } => {
435                    // Register the waker so an in-flight async native can
436                    // wake the host. A budget exhaustion re-wakes the host
437                    // immediately via `cx.waker().wake_by_ref()`.
438                    this.vm.async_waker = Some(cx.waker().clone());
439
440                    let (bootstrap_arg, ed) = if *first_slice {
441                        (closure.take(), *entry_depth)
442                    } else {
443                        (None, *entry_depth)
444                    };
445                    let ed_for_resume = *entry_depth;
446                    let outcome = this.vm.drive_one(bootstrap_arg, ed);
447                    // The first slice is consumed.
448                    *first_slice = false;
449
450                    match outcome {
451                        DispatchOutcome::Complete(values) => {
452                            // Restore JIT + clear async state.
453                            if let Some(prev) = this.saved_jit_enabled.take() {
454                                this.vm.set_jit_enabled(prev);
455                            }
456                            this.vm.async_mode = false;
457                            this.vm.async_waker = None;
458                            this.state = EvalState::Done;
459                            return Poll::Ready(Ok(values));
460                        }
461                        DispatchOutcome::Error(e) => {
462                            if let Some(prev) = this.saved_jit_enabled.take() {
463                                this.vm.set_jit_enabled(prev);
464                            }
465                            this.vm.async_mode = false;
466                            this.vm.async_waker = None;
467                            this.state = EvalState::Done;
468                            return Poll::Ready(Err(e));
469                        }
470                        DispatchOutcome::BudgetExhausted => {
471                            // Re-wake immediately so the host's executor polls
472                            // us again. The `wake_by_ref` call models "we still
473                            // have work to do but want to let other tasks run".
474                            cx.waker().wake_by_ref();
475                            return Poll::Pending;
476                        }
477                        DispatchOutcome::AsyncNativeAwaiting(fut) => {
478                            // Stash the future + flip to AwaitingNative.
479                            // Loop back to the top so the very next
480                            // iteration polls it (gives Ready-fast
481                            // futures a one-poll completion path).
482                            this.state = EvalState::AwaitingNative {
483                                entry_depth: ed_for_resume,
484                                fut,
485                            };
486                            continue;
487                        }
488                    }
489                }
490                EvalState::AwaitingNative { entry_depth, fut } => {
491                    // Poll the in-flight async native. On Ready, land
492                    // the result into the calling Lua frame and fall
493                    // back into Running so `drive_one` resumes the
494                    // dispatcher above this call site. On Pending,
495                    // surface to the host — the future itself
496                    // registered any wakers it needs inside the host
497                    // executor (e.g. a tokio timer).
498                    match fut.as_mut().poll(cx) {
499                        Poll::Ready(Ok(nret)) => {
500                            let ed = *entry_depth;
501                            // Commit may fire the
502                            // async-native "return" hook, which can
503                            // error (hook propagates `LuaError`). On
504                            // error, run the same cleanup the
505                            // `Poll::Ready(Err)` arm runs below.
506                            if let Err(e) = this.vm.commit_async_native_result(nret) {
507                                if let Some(prev) = this.saved_jit_enabled.take() {
508                                    this.vm.set_jit_enabled(prev);
509                                }
510                                this.vm.async_mode = false;
511                                this.vm.async_waker = None;
512                                this.state = EvalState::Done;
513                                return Poll::Ready(Err(e));
514                            }
515                            this.state = EvalState::Running {
516                                entry_depth: ed,
517                                first_slice: false,
518                                closure: None,
519                            };
520                            continue;
521                        }
522                        Poll::Ready(Err(e)) => {
523                            // Drop the in-flight ctx — the future
524                            // failed, so its slot is gone.
525                            this.vm.pending_async_native_ctx = None;
526                            if let Some(prev) = this.saved_jit_enabled.take() {
527                                this.vm.set_jit_enabled(prev);
528                            }
529                            this.vm.async_mode = false;
530                            this.vm.async_waker = None;
531                            this.state = EvalState::Done;
532                            return Poll::Ready(Err(e));
533                        }
534                        Poll::Pending => return Poll::Pending,
535                    }
536                }
537                EvalState::Initial { .. } => unreachable!("transitioned above"),
538                EvalState::Done => panic!("EvalFuture polled after Poll::Ready"),
539            }
540        }
541    }
542}
543
544impl<'vm> Drop for EvalFuture<'vm> {
545    fn drop(&mut self) {
546        // If the future is dropped mid-flight (host timeout, task
547        // cancelled), restore any state we mutated so the Vm is
548        // usable again. Note: stale call frames from an in-flight
549        // chunk remain in `vm.frames`; a full cleanup pass (closing
550        // `__close` handlers etc.) would mirror `close_coro` and is
551        // not done here; there is no `Vm::cancel_async`. Embedders
552        // relying on cancellation should construct a fresh Vm per request.
553        if let Some(prev) = self.saved_jit_enabled.take() {
554            self.vm.set_jit_enabled(prev);
555        }
556        // Always clear async state on drop so the next `eval` / `eval_async`
557        // call on the same Vm starts clean.
558        self.vm.async_mode = false;
559        self.vm.async_waker = None;
560        self.vm.host_yield_pending = false;
561        // Async-native bookkeeping. The future is
562        // owned by `EvalFuture` (not by the Vm) once `drive_one`
563        // surfaces it, so cancelling here only needs to clear the
564        // post-call ctx; the dropped EvalFuture takes the Pin<Box<...>>
565        // with it.
566        self.vm.pending_async_native_fut = None;
567        self.vm.pending_async_native_ctx = None;
568    }
569}