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