Skip to main content

lua_stdlib/
coro_lib.rs

1//! Coroutine library — port of `lcorolib.c`.
2//!
3//! Provides the `coroutine.*` standard-library table: `create`, `resume`,
4//! `running`, `status`, `wrap`, `yield`, `isyieldable`, and `close`.
5//!
6//! # Phase A–D stub notice
7//!
8//! Every function that requires actual coroutine execution (`resume`, `yield`,
9//! cross-thread `xmove`, `new_thread`, `close_thread`) is **unimplemented** and
10//! will panic at runtime.  The argument-checking and result-packaging logic is
11//! translated faithfully so that Phase E can drop in the real implementations
12//! without restructuring.  Phase E wires real stackful coroutines via
13//! `corosensei`.  See PORTING.md §2 #6.
14//!
15//! Translated from: `reference/lua-5.4.7/src/lcorolib.c` (210 lines, 12 functions)
16//! Target crate: `lua-stdlib`
17
18use std::cell::Cell;
19use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
20use std::sync::OnceLock;
21
22use crate::state_stub::{lua_CFunction, upvalue_index, LuaState, LuaStateStubExt as _};
23use lua_types::{error::LuaError, gc::GcRef, value::LuaValue, LuaStatus, LuaThreadClose, LuaType};
24
25thread_local! {
26    /// Per-thread suppression depth for [`LuaThreadClose`] unwind payloads.
27    ///
28    /// Incremented for the duration of each `catch_unwind` resume window by a
29    /// [`SuppressGuard`], decremented (on every path, including a panic
30    /// unwinding through the guard) when the guard drops. The process-global
31    /// chaining hook installed by [`ensure_chaining_panic_hook`] silently
32    /// swallows a `LuaThreadClose` payload only while this counter is non-zero
33    /// **on the panicking thread**, and delegates every other payload — and
34    /// `LuaThreadClose` outside a resume window — to the previously installed
35    /// hook.
36    ///
37    /// It is a counter rather than a bool because resumes nest: a coroutine
38    /// resumed from inside another resume must keep the suppression active for
39    /// the outer window after the inner one exits. Because the state is
40    /// thread-local, a `LuaThreadClose` unwind suppressed on one OS thread
41    /// never silences a simultaneous unrelated panic on another OS thread —
42    /// that thread reads its own zero counter and reaches the previous hook.
43    static THREAD_CLOSE_SUPPRESS: Cell<u32> = const { Cell::new(0) };
44}
45
46/// One-shot install guard for the process-global chaining panic hook.
47static CHAINING_HOOK_INSTALLED: OnceLock<()> = OnceLock::new();
48
49/// RAII increment of [`THREAD_CLOSE_SUPPRESS`] for one resume window.
50///
51/// Constructing the guard increments the per-thread counter; dropping it
52/// decrements. `catch_unwind` returns normally even when it catches a panic,
53/// so the decrement in `Drop` covers both the caught-panic and the
54/// normal-return paths; an uncaught panic unwinding through the guard runs the
55/// same `Drop`, so the counter invariant holds on every exit.
56struct SuppressGuard;
57
58impl SuppressGuard {
59    fn new() -> Self {
60        THREAD_CLOSE_SUPPRESS.with(|c| c.set(c.get() + 1));
61        SuppressGuard
62    }
63}
64
65impl Drop for SuppressGuard {
66    fn drop(&mut self) {
67        THREAD_CLOSE_SUPPRESS.with(|c| c.set(c.get().saturating_sub(1)));
68    }
69}
70
71/// Install — exactly once for the process — a chaining panic hook that
72/// suppresses the default panic printout for [`LuaThreadClose`] unwind
73/// payloads while a resume window is active on the panicking thread, and
74/// delegates everything else to the hook that was current at install time.
75///
76/// `LuaThreadClose` is the internal unwind used by `coroutine.close` (5.5
77/// self-close) and coroutine teardown; it is control flow, not a Rust runtime
78/// fault, so it must never reach the default printer. The previous per-resume
79/// implementation paid 3–4 heap allocations plus four global hook-lock
80/// operations on every resume to install and tear this suppression down around
81/// each `catch_unwind`. This installs the hook once and scopes the suppression
82/// with a thread-local counter ([`THREAD_CLOSE_SUPPRESS`]) instead, so the
83/// per-resume cost is two TLS counter writes.
84///
85/// Suppression is gated on the counter so it is active only inside a resume
86/// window: a `LuaThreadClose` that somehow escaped a resume would still reach
87/// the previous hook, and — because the counter is thread-local — a
88/// `LuaThreadClose` suppressed on one OS thread never silences a simultaneous
89/// unrelated panic on another OS thread.
90///
91/// Accepted tradeoff (T2-B2): an embedder that calls `std::panic::set_hook`
92/// **after** lua-rs's first resume displaces this chained hook permanently —
93/// the previous implementation re-installed the suppression on every resume,
94/// so it won each resume window even against a later embedder hook. Embedders
95/// that need a custom hook should install it before the first resume; the
96/// chaining hook then captures and delegates to it.
97fn ensure_chaining_panic_hook() {
98    CHAINING_HOOK_INSTALLED.get_or_init(|| {
99        let previous = std::panic::take_hook();
100        std::panic::set_hook(Box::new(move |info| {
101            let suppress = info.payload().downcast_ref::<LuaThreadClose>().is_some()
102                && THREAD_CLOSE_SUPPRESS.with(|c| c.get()) > 0;
103            if !suppress {
104                previous(info);
105            }
106        }));
107    });
108}
109
110// ── Coroutine status codes ────────────────────────────────────────────────────
111
112/// Coroutine is the currently running thread.
113const COS_RUN: i32 = 0;
114
115/// Coroutine has finished execution or encountered an error.
116const COS_DEAD: i32 = 1;
117
118/// Coroutine is suspended — either yielded or not yet started.
119const COS_YIELD: i32 = 2;
120
121/// Coroutine is normal — it resumed another coroutine and is waiting.
122const COS_NORM: i32 = 3;
123
124/// Human-readable status strings indexed by the `COS_*` constants above.
125/// Pushed onto the Lua stack as byte strings.
126///
127const STAT_NAMES: [&[u8]; 4] = [b"running", b"dead", b"suspended", b"normal"];
128
129// ── Registration table ────────────────────────────────────────────────────────
130
131/// Registration table for the `coroutine` standard library.
132///
133///
134/// Each entry is `(name_bytes, function_pointer)`. Phase B resolves
135/// `lua_CFunction` to the canonical type alias from `lua-types`.
136pub const CO_FUNCS: &[(&[u8], lua_CFunction)] = &[
137    (b"create", co_create),
138    (b"resume", co_resume),
139    (b"running", co_running),
140    (b"status", co_status),
141    (b"wrap", co_wrap),
142    (b"yield", co_yield),
143    (b"isyieldable", co_isyieldable),
144    (b"close", co_close),
145];
146
147// ── Internal helpers ──────────────────────────────────────────────────────────
148
149/// Retrieves the coroutine thread at stack index 1, raising a type error if
150/// the argument is absent or not a thread.
151///
152fn get_co(state: &mut LuaState) -> Result<GcRef<lua_types::value::LuaThread>, LuaError> {
153    let co = state.to_thread(1);
154    if co.is_none() {
155        let got = state.arg(1);
156        return Err(LuaError::type_arg_error(1, "thread", &got));
157    }
158    Ok(co.expect("checked above"))
159}
160
161fn get_opt_co(state: &mut LuaState) -> Result<GcRef<lua_types::value::LuaThread>, LuaError> {
162    if matches!(state.global().lua_version, lua_types::LuaVersion::V55)
163        && state.type_at(1) == LuaType::None
164    {
165        let id = state.global().current_thread_id;
166        return state
167            .global()
168            .thread_value_for(id)
169            .ok_or_else(|| LuaError::runtime(format_args!("current thread is not registered")));
170    }
171    get_co(state)
172}
173
174/// Returns one of the `COS_*` status codes describing `co` relative to the
175/// calling thread `state`. Mirrors `auxstatus` in `lcorolib.c` exactly,
176/// reading the target coroutine's `status`, call-frame depth, and stack
177/// top through `GlobalState::threads`.
178///
179/// The main thread (id 0) is never stored in the registry, so a value
180/// pointing at it is always "running" when it is the current thread.
181/// Phase E-1 cannot resume coroutines, so any registry-resident thread
182/// is either suspended (initial state, function still on stack) or dead
183/// (empty stack).
184///
185fn aux_status(state: &mut LuaState, co: &GcRef<lua_types::value::LuaThread>) -> i32 {
186    let co_id = co.id;
187    let entry_rc = {
188        let g = state.global();
189        if co_id == g.current_thread_id {
190            return COS_RUN;
191        }
192        if co_id == g.main_thread_id {
193            return COS_NORM;
194        }
195        match g.threads.get(&co_id) {
196            Some(e) => e.state.clone(),
197            None => return COS_DEAD,
198        }
199    };
200    let co_state = match entry_rc.try_borrow() {
201        Ok(state) => state,
202        Err(_) => {
203            // Nested resumes can hold a mutable borrow of a parent coroutine.
204            // In that case, the safest fallback is to report the target as
205            // "normal" (active but not suspended/dead), which matches the
206            // common nested-resume status for the parent thread.
207            return COS_NORM;
208        }
209    };
210    let raw_status = co_state.status;
211    if raw_status == LuaStatus::Yield as u8 {
212        return COS_YIELD;
213    }
214    if raw_status != LuaStatus::Ok as u8 {
215        return COS_DEAD;
216    }
217    let has_frames = co_state.ci.as_usize() > 0;
218    if has_frames {
219        return COS_NORM;
220    }
221    let ci_func = co_state.call_info[0].func.0;
222    let top = co_state.top.0;
223    let lua_gettop = top as i64 - ci_func as i64 - 1;
224    if lua_gettop == 0 {
225        COS_DEAD
226    } else {
227        COS_YIELD
228    }
229}
230
231/// Transfers `narg` arguments from `state` to `co`, resumes the coroutine,
232/// then transfers results (or error message) back to `state`.
233///
234/// Returns the number of result values (≥ 0) on success, or `-1` on error
235/// with the error object left on top of `state`'s stack.
236///
237/// Phase E-3 adds cross-thread open-upvalue mirroring around the resume
238/// boundary: before yielding control, the parent's open-upvalue values
239/// are snapshotted into `GlobalState::cross_thread_upvals` so the
240/// coroutine body can read and write them through
241/// `LuaState::upvalue_get` / `upvalue_set`. On resume return, the
242/// (possibly mutated) cache entries are flushed back into the parent's
243/// stack. This is the alternative to a stack-refactor that would let
244/// the parent's `LuaState` be reached through `Rc<RefCell<_>>` while it
245/// is held by `&mut` further up the call stack.
246///
247fn aux_resume(state: &mut LuaState, co: GcRef<lua_types::value::LuaThread>, narg: i32) -> i32 {
248    let co_id = co.id;
249    let entry_rc = {
250        let g = state.global();
251        match g.threads.get(&co_id) {
252            Some(e) => e.state.clone(),
253            None => {
254                drop(g);
255                push_lit_or_nil(state, b"cannot resume dead coroutine");
256                return -1;
257            }
258        }
259    };
260    let parent_thread_id = state.global().current_thread_id;
261    let top_before = state.get_top();
262    if top_before < narg {
263        push_lit_or_nil(state, b"not enough arguments to resume");
264        return -1;
265    }
266    let first_arg_idx = top_before - narg + 1;
267    let mut args = pop_resume_value_buf(state);
268    args.extend((first_arg_idx..=top_before).map(|i| state.value_at(i)));
269    lua_vm::api::set_top(state, (top_before - narg) as i32).ok();
270
271    let mut parent_open_upval_slots = pop_resume_slot_buf(state);
272    parent_open_upval_slots.extend(state.openupval.iter().filter_map(|uv| {
273        uv.try_open_payload()
274            .map(|(thread_id, idx)| (thread_id as u64, idx))
275    }));
276    {
277        let mut g = state.global_mut();
278        for (tid, idx) in &parent_open_upval_slots {
279            let val = state.get_at(*idx);
280            g.cross_thread_upvals.insert((*tid, *idx), val);
281        }
282    }
283
284    push_parent_gc_snapshot(state);
285
286    let (status, results_or_err): (LuaStatus, Vec<LuaValue>) = {
287        let mut co_state = match entry_rc.try_borrow_mut() {
288            Ok(b) => b,
289            Err(_) => {
290                pop_parent_gc_snapshot(state);
291                let mut g = state.global_mut();
292                for (tid, idx) in &parent_open_upval_slots {
293                    g.cross_thread_upvals.remove(&(*tid, *idx));
294                }
295                drop(g);
296                return_resume_slot_buf(state, parent_open_upval_slots);
297                return_resume_value_buf(state, args);
298                push_lit_or_nil(state, b"cannot resume non-suspended coroutine");
299                return -1;
300            }
301        };
302        if co_state.check_stack(narg + 1).is_err() {
303            drop(co_state);
304            pop_parent_gc_snapshot(state);
305            let mut g = state.global_mut();
306            for (tid, idx) in &parent_open_upval_slots {
307                g.cross_thread_upvals.remove(&(*tid, *idx));
308            }
309            drop(g);
310            return_resume_slot_buf(state, parent_open_upval_slots);
311            return_resume_value_buf(state, args);
312            push_lit_or_nil(state, b"too many arguments to resume");
313            return -1;
314        }
315        for v in args.drain(..) {
316            co_state.push(v);
317        }
318        return_resume_value_buf(state, args);
319        co_state.global_mut().current_thread_id = co_id;
320        let mut nres: i32 = 0;
321        ensure_chaining_panic_hook();
322        let resume_result = {
323            let _suppress = SuppressGuard::new();
324            catch_unwind(AssertUnwindSafe(|| {
325                lua_vm::do_::lua_resume(&mut *co_state, Some(state), narg, &mut nres)
326            }))
327        };
328        co_state.global_mut().current_thread_id = parent_thread_id;
329        let status = match resume_result {
330            Ok(status) => status,
331            Err(payload) => {
332                if let Some(close) = payload.downcast_ref::<LuaThreadClose>() {
333                    close.0
334                } else {
335                    resume_unwind(payload);
336                }
337            }
338        };
339        let co_top = co_state.top_idx().0 as i32;
340        let ci_func = co_state.current_call_info().func.0 as i32;
341        let count = if status == LuaStatus::Ok || status == LuaStatus::Yield {
342            nres
343        } else {
344            1
345        };
346        let start = co_top - count;
347        let mut vals = pop_resume_value_buf(state);
348        vals.extend((start..co_top).map(|i| co_state.get_at(lua_vm::state::StackIdx(i as u32))));
349        let new_co_top = if status == LuaStatus::Ok || status == LuaStatus::Yield {
350            (co_top - count).max(ci_func + 1)
351        } else {
352            co_top - count
353        };
354        co_state.set_top(lua_vm::state::StackIdx(new_co_top.max(0) as u32));
355        (status, vals)
356    };
357
358    // Pop the parent stack snapshot — the coroutine has yielded or returned.
359    pop_parent_gc_snapshot(state);
360
361    {
362        let mut flush = pop_resume_flush_buf(state);
363        let mut g = state.global_mut();
364        for (tid, idx) in &parent_open_upval_slots {
365            if let Some(v) = g.cross_thread_upvals.remove(&(*tid, *idx)) {
366                flush.push((*idx, v));
367            }
368        }
369        drop(g);
370        for (idx, v) in flush.drain(..) {
371            state.set_at(idx, v);
372        }
373        return_resume_flush_buf(state, flush);
374    }
375    return_resume_slot_buf(state, parent_open_upval_slots);
376
377    let mut results_or_err = results_or_err;
378    match status {
379        LuaStatus::Ok | LuaStatus::Yield => {
380            if state.check_stack(results_or_err.len() as i32 + 1).is_err() {
381                return_resume_value_buf(state, results_or_err);
382                push_lit_or_nil(state, b"too many results to resume");
383                return -1;
384            }
385            let n = results_or_err.len();
386            for v in results_or_err.drain(..) {
387                state.push(v);
388            }
389            return_resume_value_buf(state, results_or_err);
390            n as i32
391        }
392        _ => {
393            for v in results_or_err.drain(..) {
394                state.push(v);
395            }
396            return_resume_value_buf(state, results_or_err);
397            -1
398        }
399    }
400}
401
402fn push_parent_gc_snapshot(state: &mut LuaState) {
403    let top = (state.top_idx().0 as usize).min(state.stack.len());
404    let (mut stack_snapshot, mut upval_snapshot) = {
405        let mut g = state.global_mut();
406        (
407            g.snapshot_stack_pool.pop().unwrap_or_default(),
408            g.snapshot_upval_pool.pop().unwrap_or_default(),
409        )
410    };
411    stack_snapshot.extend(state.stack[..top].iter().map(|sv| sv.val));
412    upval_snapshot.extend(state.openupval.iter().cloned());
413    let mut g = state.global_mut();
414    g.suspended_parent_stacks.push(stack_snapshot);
415    g.suspended_parent_open_upvals.push(upval_snapshot);
416}
417
418fn pop_parent_gc_snapshot(state: &mut LuaState) {
419    let mut g = state.global_mut();
420    if let Some(mut v) = g.suspended_parent_open_upvals.pop() {
421        v.clear();
422        g.snapshot_upval_pool.push(v);
423    }
424    if let Some(mut v) = g.suspended_parent_stacks.pop() {
425        v.clear();
426        g.snapshot_stack_pool.push(v);
427    }
428}
429
430/// Borrow an empty open-upvalue slot buffer from the resume pool, or a fresh
431/// one if the pool is empty (first resume at this nesting depth). The returned
432/// buffer must be parked with [`return_resume_slot_buf`] on every exit path so
433/// the capacity is retained instead of freed.
434fn pop_resume_slot_buf(state: &mut LuaState) -> Vec<(u64, lua_vm::state::StackIdx)> {
435    state.global_mut().resume_upval_slot_pool.pop().unwrap_or_default()
436}
437
438/// Park a (drained) open-upvalue slot buffer back in the resume pool, clearing
439/// it first so the pooled buffer is always empty and roots nothing.
440fn return_resume_slot_buf(state: &mut LuaState, mut buf: Vec<(u64, lua_vm::state::StackIdx)>) {
441    buf.clear();
442    state.global_mut().resume_upval_slot_pool.push(buf);
443}
444
445/// Borrow an empty `LuaValue` buffer from the resume pool for an argument or
446/// result list, or a fresh one if the pool is empty (first use at this nesting
447/// depth). Park with [`return_resume_value_buf`] once the buffer is drained.
448fn pop_resume_value_buf(state: &mut LuaState) -> Vec<LuaValue> {
449    state.global_mut().resume_value_pool.pop().unwrap_or_default()
450}
451
452/// Park a (drained) `LuaValue` buffer back in the resume pool, clearing it so
453/// the pooled buffer is always empty and roots nothing.
454fn return_resume_value_buf(state: &mut LuaState, mut buf: Vec<LuaValue>) {
455    buf.clear();
456    state.global_mut().resume_value_pool.push(buf);
457}
458
459/// Borrow an empty cross-thread upvalue flush buffer from the resume pool, or a
460/// fresh one if the pool is empty. Park with [`return_resume_flush_buf`] once
461/// the buffer has been drained back onto the parent stack.
462fn pop_resume_flush_buf(state: &mut LuaState) -> Vec<(lua_vm::state::StackIdx, LuaValue)> {
463    state.global_mut().resume_flush_pool.pop().unwrap_or_default()
464}
465
466/// Park a (drained) flush buffer back in the resume pool, clearing it so the
467/// pooled buffer is always empty and roots nothing.
468fn return_resume_flush_buf(state: &mut LuaState, mut buf: Vec<(lua_vm::state::StackIdx, LuaValue)>) {
469    buf.clear();
470    state.global_mut().resume_flush_pool.push(buf);
471}
472
473/// RAII borrow of another thread's `LuaState` that keeps the thread's stack
474/// rooted while the borrow is held.
475///
476/// A coroutine whose `RefCell` is mutably borrowed at collect time cannot be
477/// traced by `trace_reachable_threads` — its stack is invisible to the
478/// marker for that whole cycle, so any object only it references is swept
479/// while still live (issue #140 bug A: `debug.traceback(co)` held the borrow
480/// across `push_vfstring`'s GC checkpoint). This guard rides the same
481/// rooting structure as `coroutine.resume`: it pushes a snapshot of the
482/// target's live stack and open upvalues onto `suspended_parent_stacks` for
483/// the lifetime of the borrow and pops it on drop. Snapshots are strictly
484/// LIFO — callers must not resume a coroutine while a guard is alive.
485///
486/// If the guarded section pushes new values onto the *target's* stack and
487/// then allocates before consuming them (`lua_getinfo`'s 'L'/'f' pushes),
488/// call [`RootedThreadBorrow::resnapshot`] after the pushes so the snapshot
489/// covers them too.
490pub(crate) struct RootedThreadBorrow<'a> {
491    inner: std::cell::RefMut<'a, LuaState>,
492}
493
494impl RootedThreadBorrow<'_> {
495    /// Re-copy the target's current live stack and open upvalues into the
496    /// snapshot pushed at borrow time, covering values pushed onto the
497    /// target since then.
498    pub(crate) fn resnapshot(&mut self) {
499        let top = (self.inner.top_idx().0 as usize).min(self.inner.stack.len());
500        let stack_copy: Vec<LuaValue> = self.inner.stack[..top].iter().map(|sv| sv.val).collect();
501        let upval_copy: Vec<GcRef<lua_types::UpVal>> = self.inner.openupval.to_vec();
502        let mut g = self.inner.global_mut();
503        if let Some(slot) = g.suspended_parent_stacks.last_mut() {
504            slot.clear();
505            slot.extend(stack_copy);
506        }
507        if let Some(slot) = g.suspended_parent_open_upvals.last_mut() {
508            slot.clear();
509            slot.extend(upval_copy);
510        }
511    }
512}
513
514impl std::ops::Deref for RootedThreadBorrow<'_> {
515    type Target = LuaState;
516    fn deref(&self) -> &LuaState {
517        &self.inner
518    }
519}
520
521impl std::ops::DerefMut for RootedThreadBorrow<'_> {
522    fn deref_mut(&mut self) -> &mut LuaState {
523        &mut self.inner
524    }
525}
526
527impl Drop for RootedThreadBorrow<'_> {
528    fn drop(&mut self) {
529        let mut g = self.inner.global_mut();
530        if let Some(mut v) = g.suspended_parent_open_upvals.pop() {
531            v.clear();
532            g.snapshot_upval_pool.push(v);
533        }
534        if let Some(mut v) = g.suspended_parent_stacks.pop() {
535            v.clear();
536            g.snapshot_stack_pool.push(v);
537        }
538    }
539}
540
541/// Borrow `cell`'s thread state mutably with its stack rooted for the
542/// duration (see [`RootedThreadBorrow`]). Panics if the cell is already
543/// borrowed, matching the bare `borrow_mut()` call sites this replaces.
544pub(crate) fn borrow_thread_rooted<'a>(
545    state: &mut LuaState,
546    cell: &'a std::cell::RefCell<LuaState>,
547) -> RootedThreadBorrow<'a> {
548    let inner = cell.borrow_mut();
549    let top = (inner.top_idx().0 as usize).min(inner.stack.len());
550    let (mut stack_snapshot, mut upval_snapshot) = {
551        let mut g = state.global_mut();
552        (
553            g.snapshot_stack_pool.pop().unwrap_or_default(),
554            g.snapshot_upval_pool.pop().unwrap_or_default(),
555        )
556    };
557    stack_snapshot.extend(inner.stack[..top].iter().map(|sv| sv.val));
558    upval_snapshot.extend(inner.openupval.iter().cloned());
559    let mut g = state.global_mut();
560    g.suspended_parent_stacks.push(stack_snapshot);
561    g.suspended_parent_open_upvals.push(upval_snapshot);
562    drop(g);
563    RootedThreadBorrow { inner }
564}
565
566/// Helper: push a string literal or fall back to Nil on intern failure.
567fn push_lit_or_nil(state: &mut LuaState, bytes: &[u8]) {
568    match state.intern_str(bytes) {
569        Ok(s) => state.push(LuaValue::Str(s)),
570        Err(_) => state.push(LuaValue::Nil),
571    }
572}
573
574// ── Public library functions ──────────────────────────────────────────────────
575
576/// `coroutine.resume(co [, val1, ...])` — attempt to resume coroutine `co`.
577///
578/// On success pushes `true` followed by all values yielded or returned by `co`.
579/// On failure pushes `false` followed by the error object.
580///
581pub fn co_resume(state: &mut LuaState) -> Result<usize, LuaError> {
582    let co = get_co(state)?;
583    // PORT NOTE: lua_gettop returns the argument count; -1 excludes the coroutine
584    // itself which sits at index 1.
585    let narg = state.get_top() - 1;
586    let r = aux_resume(state, co, narg);
587    if r < 0 {
588        // A sandbox budget trip is uncatchable: re-raise into the caller frame
589        // instead of returning `false, msg`, so code cannot keep a runaway
590        // coroutine alive by resuming it in a loop.
591        if state.sandbox_aborting() {
592            let top = state.get_top();
593            let err_val = state.value_at(top);
594            return Err(LuaError::from_value(err_val));
595        }
596        state.push(LuaValue::Bool(false));
597        state.insert(-2)?;
598        Ok(2)
599    } else {
600        state.push(LuaValue::Bool(true));
601        state.insert(-(r + 1))?;
602        Ok((r + 1) as usize)
603    }
604}
605
606/// Closure body installed by `coroutine.wrap`. The wrapped coroutine
607/// thread is stored in upvalue slot 1 as a `LuaValue::Thread`.
608///
609/// On call: forwards all args to `aux_resume` on the captured thread. On
610/// success returns the yielded/returned values; on coroutine error raises
611/// the error (matching `select(2, assert(resume(co, ...)))` semantics).
612///
613fn aux_wrap(state: &mut LuaState) -> Result<usize, LuaError> {
614    let up = state.value_at(upvalue_index(1));
615    let co = match up {
616        LuaValue::Thread(t) => t,
617        _ => {
618            return Err(LuaError::runtime(format_args!(
619                "coroutine.wrap: upvalue is not a thread"
620            )))
621        }
622    };
623    let narg = state.get_top();
624    let r = aux_resume(state, co.clone(), narg);
625    if r < 0 {
626        let top = state.get_top();
627        let mut err_val = state.value_at(top);
628        if aux_status(state, &co) == COS_DEAD {
629            let old_err = state.pop();
630            let nclose = close_suspended_or_dead(state, co)?;
631            err_val = if nclose >= 2 {
632                let top = state.get_top();
633                state.value_at(top)
634            } else {
635                old_err
636            };
637            state.pop_n(nclose);
638        }
639        Err(LuaError::from_value(err_val))
640    } else {
641        Ok(r as usize)
642    }
643}
644
645/// `coroutine.create(f)` — create a new coroutine that will run function `f`.
646///
647/// Pushes the new thread value and returns 1.
648///
649/// Phase E-1: allocates a real `LuaState` registered in
650/// `GlobalState::threads`, with `f` staged on the new thread's stack so
651/// `coroutine.status` reports `"suspended"`. The full `xmove` from the
652/// caller's stack arrives in slice 02b; for this slice the body is
653/// cloned via `value_at(1)`, which has the same net stack effect since
654/// `lua_newthread` in C also leaves only the thread value on the
655/// caller's stack.
656///
657pub fn co_create(state: &mut LuaState) -> Result<usize, LuaError> {
658    state.check_arg_type(1, LuaType::Function)?;
659    let body = state.value_at(1);
660    let _nl = state.new_thread(Some(body))?;
661    Ok(1)
662}
663
664/// `coroutine.wrap(f)` — create a coroutine and return a resuming function.
665///
666/// The returned function, when called, resumes the coroutine as if by
667/// `coroutine.resume`, but raises an error rather than returning `false`.
668///
669///
670/// Captures the new coroutine thread as upvalue 1 of `aux_wrap`.
671pub fn co_wrap(state: &mut LuaState) -> Result<usize, LuaError> {
672    co_create(state)?;
673    state.push_cclosure(aux_wrap, 1)?;
674    Ok(1)
675}
676
677/// `coroutine.yield([...])` — suspend the running coroutine.
678///
679/// All arguments are passed back as results of the corresponding `resume`.
680///
681/// → `return lua_yield(L, lua_gettop(L));`
682/// → `lua_yield(L,n)` is `lua_yieldk(L, n, 0, NULL)` (lua.h:316)
683pub fn co_yield(state: &mut LuaState) -> Result<usize, LuaError> {
684    let n = state.get_top();
685    let r = lua_vm::do_::lua_yieldk(state, n, 0, None)?;
686    Ok(r as usize)
687}
688
689/// `coroutine.status(co)` — return a string describing `co`'s current status.
690///
691/// Returns one of `"running"`, `"dead"`, `"suspended"`, or `"normal"`.
692///
693pub fn co_status(state: &mut LuaState) -> Result<usize, LuaError> {
694    let co = get_co(state)?;
695    let idx = aux_status(state, &co) as usize;
696    let name: &[u8] = STAT_NAMES[idx];
697    let interned = state.intern_str(name)?;
698    state.push(LuaValue::Str(interned));
699    Ok(1)
700}
701
702/// `coroutine.isyieldable([co])` — test whether a coroutine (default: current)
703/// is in a yieldable state.
704///
705pub fn co_isyieldable(state: &mut LuaState) -> Result<usize, LuaError> {
706    let is_yieldable = if matches!(state.type_at(1), LuaType::None) {
707        state.is_yieldable()
708    } else {
709        let co = get_co(state)?;
710        let co_id = co.id;
711        let (is_main, is_current) = {
712            let g = state.global();
713            (co_id == g.main_thread_id, co_id == g.current_thread_id)
714        };
715        if is_main {
716            false
717        } else if is_current {
718            state.is_yieldable()
719        } else {
720            let entry_rc = {
721                let g = state.global();
722                g.threads
723                    .get(&co_id)
724                    .expect("thread value carries an id that must resolve in GlobalState::threads")
725                    .state
726                    .clone()
727            };
728            let target_is_yieldable = match entry_rc.try_borrow() {
729                Ok(b) => b.is_yieldable(),
730                Err(_) => false,
731            };
732            target_is_yieldable
733        }
734    };
735    state.push(LuaValue::Bool(is_yieldable));
736    Ok(1)
737}
738
739/// `coroutine.running()` — return the current coroutine plus a boolean.
740///
741/// The boolean is `true` when the current coroutine is the main thread.
742///
743pub fn co_running(state: &mut LuaState) -> Result<usize, LuaError> {
744    // TODO(port): push_thread pushes a Thread value for the current LuaState and
745    // returns true iff it is the main thread; Phase B wire-up needed.
746    let is_main = state.push_thread()?;
747    // Lua 5.1's `coroutine.running()` returns nil in the main coroutine and only
748    // the running thread (one value) inside a coroutine — the second `is-main`
749    // boolean is a 5.2 addition. Verified against lua5.1.5:
750    // `coroutine.running()` in main prints `nil`. See
751    // specs/followup/5.1-roster-syntax.md §1.
752    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
753        if is_main {
754            state.pop_n(1);
755            state.push(LuaValue::Nil);
756        }
757        return Ok(1);
758    }
759    state.push(LuaValue::Bool(is_main));
760    Ok(2)
761}
762
763/// `coroutine.close(co)` — close a dead or suspended coroutine.
764///
765/// Closes a coroutine, running any pending to-be-closed variables via
766/// `__close` and resetting its status. Valid only when the target is
767/// suspended (`Yield`) or dead (`Ok` with no active frames).
768/// Calling on a running or normal coroutine raises an error.
769///
770pub fn co_close(state: &mut LuaState) -> Result<usize, LuaError> {
771    lua_vm::state::inc_c_stack(state)?;
772    let result = (|| {
773        let co = get_opt_co(state)?;
774        let status = aux_status(state, &co);
775        match status {
776            COS_DEAD | COS_YIELD => close_suspended_or_dead(state, co),
777            _ => {
778                if matches!(state.global().lua_version, lua_types::LuaVersion::V55)
779                    && status == COS_RUN
780                    && state.global().closing_thread_id == Some(co.id)
781                {
782                    state.push(LuaValue::Bool(true));
783                    return Ok(1);
784                }
785                if matches!(state.global().lua_version, lua_types::LuaVersion::V55)
786                    && status == COS_RUN
787                    && co.id == state.global().main_thread_id
788                {
789                    return Err(LuaError::runtime(format_args!("cannot close main thread")));
790                }
791                if matches!(state.global().lua_version, lua_types::LuaVersion::V55)
792                    && status == COS_RUN
793                    && co.id == state.global().current_thread_id
794                {
795                    state.global_mut().closing_thread_id = Some(co.id);
796                    let in_status = state.status as i32;
797                    let s = lua_vm::state::reset_thread(state, in_status);
798                    state.global_mut().closing_thread_id = None;
799                    state.n_ccalls = state.n_ccalls.saturating_sub(1);
800                    std::panic::panic_any(LuaThreadClose(LuaStatus::from_raw(s)));
801                }
802                let name = if status == COS_RUN {
803                    "running"
804                } else {
805                    "normal"
806                };
807                Err(LuaError::runtime(format_args!(
808                    "cannot close a {} coroutine",
809                    name
810                )))
811            }
812        }
813    })();
814    state.n_ccalls -= 1;
815    result
816}
817
818/// Performs the actual close for a suspended or dead coroutine.
819fn close_suspended_or_dead(
820    state: &mut LuaState,
821    co: GcRef<lua_types::value::LuaThread>,
822) -> Result<usize, LuaError> {
823    let co_id = co.id;
824    let entry_rc_opt = {
825        let g = state.global();
826        g.threads.get(&co_id).map(|e| e.state.clone())
827    };
828    let entry_rc = match entry_rc_opt {
829        Some(rc) => rc,
830        None => {
831            state.push(LuaValue::Bool(true));
832            return Ok(1);
833        }
834    };
835    let parent_thread_id = state.global().current_thread_id;
836    let caller_c_calls = state.c_calls();
837
838    let mut parent_open_upval_slots = pop_resume_slot_buf(state);
839    parent_open_upval_slots.extend(state.openupval.iter().filter_map(|uv| {
840        uv.try_open_payload()
841            .map(|(thread_id, idx)| (thread_id as u64, idx))
842    }));
843    {
844        let mut g = state.global_mut();
845        for (tid, idx) in &parent_open_upval_slots {
846            let val = state.get_at(*idx);
847            g.cross_thread_upvals.insert((*tid, *idx), val);
848        }
849    }
850
851    push_parent_gc_snapshot(state);
852
853    let (status, err_value): (i32, Option<LuaValue>) = {
854        let mut co_state = entry_rc.borrow_mut();
855        co_state.global_mut().current_thread_id = co_id;
856        co_state.global_mut().closing_thread_id = Some(co_id);
857        co_state.n_ccalls = caller_c_calls;
858        let in_status = co_state.status as i32;
859        let s = lua_vm::state::reset_thread(&mut *co_state, in_status);
860        co_state.global_mut().closing_thread_id = None;
861        co_state.global_mut().current_thread_id = parent_thread_id;
862        if s == LuaStatus::Ok as i32 {
863            (s, None)
864        } else {
865            let top = co_state.top_idx().0;
866            if top > 0 {
867                let err = co_state.get_at(lua_vm::state::StackIdx(top - 1));
868                co_state.set_top(lua_vm::state::StackIdx(top - 1));
869                (s, Some(err))
870            } else {
871                (s, Some(LuaValue::Nil))
872            }
873        }
874    };
875
876    pop_parent_gc_snapshot(state);
877
878    {
879        let mut flush = pop_resume_flush_buf(state);
880        let mut g = state.global_mut();
881        for (tid, idx) in &parent_open_upval_slots {
882            if let Some(v) = g.cross_thread_upvals.remove(&(*tid, *idx)) {
883                flush.push((*idx, v));
884            }
885        }
886        drop(g);
887        for (idx, v) in flush.drain(..) {
888            state.set_at(idx, v);
889        }
890        return_resume_flush_buf(state, flush);
891    }
892    return_resume_slot_buf(state, parent_open_upval_slots);
893
894    if status == LuaStatus::Ok as i32 {
895        state.push(LuaValue::Bool(true));
896        Ok(1)
897    } else {
898        state.push(LuaValue::Bool(false));
899        if let Some(v) = err_value {
900            state.push(v);
901        } else {
902            state.push(LuaValue::Nil);
903        }
904        Ok(2)
905    }
906}
907
908// ── Module entry point ────────────────────────────────────────────────────────
909
910/// Opens the `coroutine` standard library by pushing a new table containing
911/// all `coroutine.*` functions.
912///
913pub fn open_coroutine(state: &mut LuaState) -> Result<usize, LuaError> {
914    // `coroutine.close` is a Lua 5.4 addition tied to to-be-closed variables
915    // (`specs/research/5.3-upstream-delta.md` delta #9). Under 5.3 it is absent
916    // from the roster entirely.
917    use lua_types::LuaVersion;
918    let version = state.global().lua_version;
919    let has_close = !matches!(version, LuaVersion::V51 | LuaVersion::V52 | LuaVersion::V53);
920    // `coroutine.isyieldable` is a Lua 5.3 addition; it is absent in 5.1 and 5.2
921    // (verified against lua5.1.5 and lua5.2.4: `type(coroutine.isyieldable)` ==
922    // "nil"). See specs/followup/5.1-roster-syntax.md §1.
923    let has_isyieldable = !matches!(version, LuaVersion::V51 | LuaVersion::V52);
924    if has_close && has_isyieldable {
925        state.new_lib(CO_FUNCS)?;
926    } else {
927        let filtered: Vec<(&[u8], lua_CFunction)> = CO_FUNCS
928            .iter()
929            .filter(|(name, _)| {
930                (has_close || *name != b"close".as_slice())
931                    && (has_isyieldable || *name != b"isyieldable".as_slice())
932            })
933            .copied()
934            .collect();
935        state.new_lib(&filtered)?;
936    }
937    Ok(1)
938}
939
940// ──────────────────────────────────────────────────────────────────────────────
941// PORT STATUS
942//   source:        src/lcorolib.c  (210 lines, 12 functions)
943//   target_crate:  lua-stdlib
944//   confidence:    medium
945//   todos:         21
946//   port_notes:    2
947//   unsafe_blocks: 0
948//   notes:         All coroutine execution primitives (resume, yield, xmove,
949//                  new_thread, close_thread) are Phase E stubs that panic.
950//                  Argument-checking / result-packaging logic is faithfully
951//                  translated so Phase E can drop in real implementations.
952//                  The CO_FUNCS table type references lua_CFunction which is
953//                  resolved in Phase B.  LuaState / GcRef<LuaState> / LuaStatus
954//                  imports are all deferred to Phase B.
955// ──────────────────────────────────────────────────────────────────────────────