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::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
19use std::sync::{Arc, Mutex};
20
21use crate::state_stub::{lua_CFunction, upvalue_index, LuaState, LuaStateStubExt as _};
22use lua_types::{error::LuaError, gc::GcRef, value::LuaValue, LuaStatus, LuaThreadClose, LuaType};
23
24// ── Coroutine status codes ────────────────────────────────────────────────────
25
26/// Coroutine is the currently running thread.
27const COS_RUN: i32 = 0;
28
29/// Coroutine has finished execution or encountered an error.
30const COS_DEAD: i32 = 1;
31
32/// Coroutine is suspended — either yielded or not yet started.
33const COS_YIELD: i32 = 2;
34
35/// Coroutine is normal — it resumed another coroutine and is waiting.
36const COS_NORM: i32 = 3;
37
38/// Human-readable status strings indexed by the `COS_*` constants above.
39/// Pushed onto the Lua stack as byte strings.
40///
41const STAT_NAMES: [&[u8]; 4] = [b"running", b"dead", b"suspended", b"normal"];
42
43// ── Registration table ────────────────────────────────────────────────────────
44
45/// Registration table for the `coroutine` standard library.
46///
47///
48/// Each entry is `(name_bytes, function_pointer)`. Phase B resolves
49/// `lua_CFunction` to the canonical type alias from `lua-types`.
50pub const CO_FUNCS: &[(&[u8], lua_CFunction)] = &[
51    (b"create", co_create),
52    (b"resume", co_resume),
53    (b"running", co_running),
54    (b"status", co_status),
55    (b"wrap", co_wrap),
56    (b"yield", co_yield),
57    (b"isyieldable", co_isyieldable),
58    (b"close", co_close),
59];
60
61// ── Internal helpers ──────────────────────────────────────────────────────────
62
63/// Retrieves the coroutine thread at stack index 1, raising a type error if
64/// the argument is absent or not a thread.
65///
66fn get_co(state: &mut LuaState) -> Result<GcRef<lua_types::value::LuaThread>, LuaError> {
67    let co = state.to_thread(1);
68    if co.is_none() {
69        let got = state.arg(1);
70        return Err(LuaError::type_arg_error(1, "thread", &got));
71    }
72    Ok(co.expect("checked above"))
73}
74
75fn get_opt_co(state: &mut LuaState) -> Result<GcRef<lua_types::value::LuaThread>, LuaError> {
76    if matches!(state.global().lua_version, lua_types::LuaVersion::V55)
77        && state.type_at(1) == LuaType::None
78    {
79        let id = state.global().current_thread_id;
80        return state
81            .global()
82            .thread_value_for(id)
83            .ok_or_else(|| LuaError::runtime(format_args!("current thread is not registered")));
84    }
85    get_co(state)
86}
87
88/// Returns one of the `COS_*` status codes describing `co` relative to the
89/// calling thread `state`. Mirrors `auxstatus` in `lcorolib.c` exactly,
90/// reading the target coroutine's `status`, call-frame depth, and stack
91/// top through `GlobalState::threads`.
92///
93/// The main thread (id 0) is never stored in the registry, so a value
94/// pointing at it is always "running" when it is the current thread.
95/// Phase E-1 cannot resume coroutines, so any registry-resident thread
96/// is either suspended (initial state, function still on stack) or dead
97/// (empty stack).
98///
99fn aux_status(state: &mut LuaState, co: &GcRef<lua_types::value::LuaThread>) -> i32 {
100    let co_id = co.id;
101    let entry_rc = {
102        let g = state.global();
103        if co_id == g.current_thread_id {
104            return COS_RUN;
105        }
106        if co_id == g.main_thread_id {
107            return COS_NORM;
108        }
109        match g.threads.get(&co_id) {
110            Some(e) => e.state.clone(),
111            None => return COS_DEAD,
112        }
113    };
114    let co_state = match entry_rc.try_borrow() {
115        Ok(state) => state,
116        Err(_) => {
117            // Nested resumes can hold a mutable borrow of a parent coroutine.
118            // In that case, the safest fallback is to report the target as
119            // "normal" (active but not suspended/dead), which matches the
120            // common nested-resume status for the parent thread.
121            return COS_NORM;
122        }
123    };
124    let raw_status = co_state.status;
125    if raw_status == LuaStatus::Yield as u8 {
126        return COS_YIELD;
127    }
128    if raw_status != LuaStatus::Ok as u8 {
129        return COS_DEAD;
130    }
131    let has_frames = co_state.ci.as_usize() > 0;
132    if has_frames {
133        return COS_NORM;
134    }
135    let ci_func = co_state.call_info[0].func.0;
136    let top = co_state.top.0;
137    let lua_gettop = top as i64 - ci_func as i64 - 1;
138    if lua_gettop == 0 {
139        COS_DEAD
140    } else {
141        COS_YIELD
142    }
143}
144
145/// Transfers `narg` arguments from `state` to `co`, resumes the coroutine,
146/// then transfers results (or error message) back to `state`.
147///
148/// Returns the number of result values (≥ 0) on success, or `-1` on error
149/// with the error object left on top of `state`'s stack.
150///
151/// Phase E-3 adds cross-thread open-upvalue mirroring around the resume
152/// boundary: before yielding control, the parent's open-upvalue values
153/// are snapshotted into `GlobalState::cross_thread_upvals` so the
154/// coroutine body can read and write them through
155/// `LuaState::upvalue_get` / `upvalue_set`. On resume return, the
156/// (possibly mutated) cache entries are flushed back into the parent's
157/// stack. This is the alternative to a stack-refactor that would let
158/// the parent's `LuaState` be reached through `Rc<RefCell<_>>` while it
159/// is held by `&mut` further up the call stack.
160///
161fn aux_resume(state: &mut LuaState, co: GcRef<lua_types::value::LuaThread>, narg: i32) -> i32 {
162    let co_id = co.id;
163    let entry_rc = {
164        let g = state.global();
165        match g.threads.get(&co_id) {
166            Some(e) => e.state.clone(),
167            None => {
168                drop(g);
169                push_lit_or_nil(state, b"cannot resume dead coroutine");
170                return -1;
171            }
172        }
173    };
174    let parent_thread_id = state.global().current_thread_id;
175    let top_before = state.get_top();
176    if top_before < narg {
177        push_lit_or_nil(state, b"not enough arguments to resume");
178        return -1;
179    }
180    let first_arg_idx = top_before - narg + 1;
181    let args: Vec<LuaValue> = (first_arg_idx..=top_before)
182        .map(|i| state.value_at(i))
183        .collect();
184    lua_vm::api::set_top(state, (top_before - narg) as i32).ok();
185
186    let parent_open_upval_slots: Vec<(u64, lua_vm::state::StackIdx)> = state
187        .openupval
188        .iter()
189        .filter_map(|uv| match &*uv.slot() {
190            lua_types::UpValState::Open { thread_id, idx } => Some((*thread_id as u64, *idx)),
191            lua_types::UpValState::Closed(_) => None,
192        })
193        .collect();
194    {
195        let mut g = state.global_mut();
196        for (tid, idx) in &parent_open_upval_slots {
197            let val = state.get_at(*idx);
198            g.cross_thread_upvals.insert((*tid, *idx), val);
199        }
200    }
201
202    push_parent_gc_snapshot(state);
203
204    let (status, results_or_err): (LuaStatus, Vec<LuaValue>) = {
205        let mut co_state = match entry_rc.try_borrow_mut() {
206            Ok(b) => b,
207            Err(_) => {
208                pop_parent_gc_snapshot(state);
209                let mut g = state.global_mut();
210                for (tid, idx) in &parent_open_upval_slots {
211                    g.cross_thread_upvals.remove(&(*tid, *idx));
212                }
213                drop(g);
214                push_lit_or_nil(state, b"cannot resume non-suspended coroutine");
215                return -1;
216            }
217        };
218        if co_state.check_stack(narg + 1).is_err() {
219            drop(co_state);
220            pop_parent_gc_snapshot(state);
221            let mut g = state.global_mut();
222            for (tid, idx) in &parent_open_upval_slots {
223                g.cross_thread_upvals.remove(&(*tid, *idx));
224            }
225            drop(g);
226            push_lit_or_nil(state, b"too many arguments to resume");
227            return -1;
228        }
229        for v in args {
230            co_state.push(v);
231        }
232        co_state.global_mut().current_thread_id = co_id;
233        let mut nres: i32 = 0;
234        let previous_hook = Arc::new(Mutex::new(Some(std::panic::take_hook())));
235        let previous_for_hook = Arc::clone(&previous_hook);
236        std::panic::set_hook(Box::new(move |info| {
237            if info.payload().downcast_ref::<LuaThreadClose>().is_none() {
238                if let Ok(guard) = previous_for_hook.lock() {
239                    if let Some(hook) = guard.as_ref() {
240                        hook(info);
241                    }
242                }
243            }
244        }));
245        let resume_result = catch_unwind(AssertUnwindSafe(|| {
246            lua_vm::do_::lua_resume(&mut *co_state, Some(state), narg, &mut nres)
247        }));
248        let _installed_hook = std::panic::take_hook();
249        if let Some(hook) = previous_hook.lock().ok().and_then(|mut h| h.take()) {
250            std::panic::set_hook(hook);
251        }
252        co_state.global_mut().current_thread_id = parent_thread_id;
253        let status = match resume_result {
254            Ok(status) => status,
255            Err(payload) => {
256                if let Some(close) = payload.downcast_ref::<LuaThreadClose>() {
257                    close.0
258                } else {
259                    resume_unwind(payload);
260                }
261            }
262        };
263        let co_top = co_state.top_idx().0 as i32;
264        let ci_func = co_state.current_call_info().func.0 as i32;
265        let count = if status == LuaStatus::Ok || status == LuaStatus::Yield {
266            nres
267        } else {
268            1
269        };
270        let start = co_top - count;
271        let vals: Vec<LuaValue> = (start..co_top)
272            .map(|i| co_state.get_at(lua_vm::state::StackIdx(i as u32)))
273            .collect();
274        let new_co_top = if status == LuaStatus::Ok || status == LuaStatus::Yield {
275            (co_top - count).max(ci_func + 1)
276        } else {
277            co_top - count
278        };
279        co_state.set_top(lua_vm::state::StackIdx(new_co_top.max(0) as u32));
280        (status, vals)
281    };
282
283    // Pop the parent stack snapshot — the coroutine has yielded or returned.
284    pop_parent_gc_snapshot(state);
285
286    {
287        let mut g = state.global_mut();
288        let mut flush: Vec<(lua_vm::state::StackIdx, LuaValue)> = Vec::new();
289        for (tid, idx) in &parent_open_upval_slots {
290            if let Some(v) = g.cross_thread_upvals.remove(&(*tid, *idx)) {
291                flush.push((*idx, v));
292            }
293        }
294        drop(g);
295        for (idx, v) in flush {
296            state.set_at(idx, v);
297        }
298    }
299
300    match status {
301        LuaStatus::Ok | LuaStatus::Yield => {
302            if state.check_stack(results_or_err.len() as i32 + 1).is_err() {
303                push_lit_or_nil(state, b"too many results to resume");
304                return -1;
305            }
306            let n = results_or_err.len();
307            for v in results_or_err {
308                state.push(v);
309            }
310            n as i32
311        }
312        _ => {
313            for v in results_or_err {
314                state.push(v);
315            }
316            -1
317        }
318    }
319}
320
321fn push_parent_gc_snapshot(state: &mut LuaState) {
322    let top = state.top_idx();
323    let stack_snapshot: Vec<LuaValue> = (0..top.0)
324        .map(|i| state.get_at(lua_vm::state::StackIdx(i)))
325        .collect();
326    let open_upval_snapshot = state.openupval.clone();
327    let mut g = state.global_mut();
328    g.suspended_parent_stacks.push(stack_snapshot);
329    g.suspended_parent_open_upvals.push(open_upval_snapshot);
330}
331
332fn pop_parent_gc_snapshot(state: &mut LuaState) {
333    let mut g = state.global_mut();
334    g.suspended_parent_open_upvals.pop();
335    g.suspended_parent_stacks.pop();
336}
337
338/// Helper: push a string literal or fall back to Nil on intern failure.
339fn push_lit_or_nil(state: &mut LuaState, bytes: &[u8]) {
340    match state.intern_str(bytes) {
341        Ok(s) => state.push(LuaValue::Str(s)),
342        Err(_) => state.push(LuaValue::Nil),
343    }
344}
345
346// ── Public library functions ──────────────────────────────────────────────────
347
348/// `coroutine.resume(co [, val1, ...])` — attempt to resume coroutine `co`.
349///
350/// On success pushes `true` followed by all values yielded or returned by `co`.
351/// On failure pushes `false` followed by the error object.
352///
353pub fn co_resume(state: &mut LuaState) -> Result<usize, LuaError> {
354    let co = get_co(state)?;
355    // PORT NOTE: lua_gettop returns the argument count; -1 excludes the coroutine
356    // itself which sits at index 1.
357    let narg = state.get_top() - 1;
358    let r = aux_resume(state, co, narg);
359    if r < 0 {
360        // A sandbox budget trip is uncatchable: re-raise into the caller frame
361        // instead of returning `false, msg`, so code cannot keep a runaway
362        // coroutine alive by resuming it in a loop.
363        if state.sandbox_aborting() {
364            let top = state.get_top();
365            let err_val = state.value_at(top);
366            return Err(LuaError::from_value(err_val));
367        }
368        state.push(LuaValue::Bool(false));
369        state.insert(-2)?;
370        Ok(2)
371    } else {
372        state.push(LuaValue::Bool(true));
373        state.insert(-(r + 1))?;
374        Ok((r + 1) as usize)
375    }
376}
377
378/// Closure body installed by `coroutine.wrap`. The wrapped coroutine
379/// thread is stored in upvalue slot 1 as a `LuaValue::Thread`.
380///
381/// On call: forwards all args to `aux_resume` on the captured thread. On
382/// success returns the yielded/returned values; on coroutine error raises
383/// the error (matching `select(2, assert(resume(co, ...)))` semantics).
384///
385fn aux_wrap(state: &mut LuaState) -> Result<usize, LuaError> {
386    let up = state.value_at(upvalue_index(1));
387    let co = match up {
388        LuaValue::Thread(t) => t,
389        _ => {
390            return Err(LuaError::runtime(format_args!(
391                "coroutine.wrap: upvalue is not a thread"
392            )))
393        }
394    };
395    let narg = state.get_top();
396    let r = aux_resume(state, co.clone(), narg);
397    if r < 0 {
398        let top = state.get_top();
399        let mut err_val = state.value_at(top);
400        if aux_status(state, &co) == COS_DEAD {
401            let old_err = state.pop();
402            let nclose = close_suspended_or_dead(state, co)?;
403            err_val = if nclose >= 2 {
404                let top = state.get_top();
405                state.value_at(top)
406            } else {
407                old_err
408            };
409            state.pop_n(nclose);
410        }
411        Err(LuaError::from_value(err_val))
412    } else {
413        Ok(r as usize)
414    }
415}
416
417/// `coroutine.create(f)` — create a new coroutine that will run function `f`.
418///
419/// Pushes the new thread value and returns 1.
420///
421/// Phase E-1: allocates a real `LuaState` registered in
422/// `GlobalState::threads`, with `f` staged on the new thread's stack so
423/// `coroutine.status` reports `"suspended"`. The full `xmove` from the
424/// caller's stack arrives in slice 02b; for this slice the body is
425/// cloned via `value_at(1)`, which has the same net stack effect since
426/// `lua_newthread` in C also leaves only the thread value on the
427/// caller's stack.
428///
429pub fn co_create(state: &mut LuaState) -> Result<usize, LuaError> {
430    state.check_arg_type(1, LuaType::Function)?;
431    let body = state.value_at(1);
432    let _nl = state.new_thread(Some(body))?;
433    Ok(1)
434}
435
436/// `coroutine.wrap(f)` — create a coroutine and return a resuming function.
437///
438/// The returned function, when called, resumes the coroutine as if by
439/// `coroutine.resume`, but raises an error rather than returning `false`.
440///
441///
442/// Captures the new coroutine thread as upvalue 1 of `aux_wrap`.
443pub fn co_wrap(state: &mut LuaState) -> Result<usize, LuaError> {
444    co_create(state)?;
445    state.push_cclosure(aux_wrap, 1)?;
446    Ok(1)
447}
448
449/// `coroutine.yield([...])` — suspend the running coroutine.
450///
451/// All arguments are passed back as results of the corresponding `resume`.
452///
453/// → `return lua_yield(L, lua_gettop(L));`
454/// → `lua_yield(L,n)` is `lua_yieldk(L, n, 0, NULL)` (lua.h:316)
455pub fn co_yield(state: &mut LuaState) -> Result<usize, LuaError> {
456    let n = state.get_top();
457    let r = lua_vm::do_::lua_yieldk(state, n, 0, None)?;
458    Ok(r as usize)
459}
460
461/// `coroutine.status(co)` — return a string describing `co`'s current status.
462///
463/// Returns one of `"running"`, `"dead"`, `"suspended"`, or `"normal"`.
464///
465pub fn co_status(state: &mut LuaState) -> Result<usize, LuaError> {
466    let co = get_co(state)?;
467    let idx = aux_status(state, &co) as usize;
468    let name: &[u8] = STAT_NAMES[idx];
469    let interned = state.intern_str(name)?;
470    state.push(LuaValue::Str(interned));
471    Ok(1)
472}
473
474/// `coroutine.isyieldable([co])` — test whether a coroutine (default: current)
475/// is in a yieldable state.
476///
477pub fn co_isyieldable(state: &mut LuaState) -> Result<usize, LuaError> {
478    let is_yieldable = if matches!(state.type_at(1), LuaType::None) {
479        state.is_yieldable()
480    } else {
481        let co = get_co(state)?;
482        let co_id = co.id;
483        let (is_main, is_current) = {
484            let g = state.global();
485            (co_id == g.main_thread_id, co_id == g.current_thread_id)
486        };
487        if is_main {
488            false
489        } else if is_current {
490            state.is_yieldable()
491        } else {
492            let entry_rc = {
493                let g = state.global();
494                g.threads
495                    .get(&co_id)
496                    .expect("thread value carries an id that must resolve in GlobalState::threads")
497                    .state
498                    .clone()
499            };
500            let target_is_yieldable = match entry_rc.try_borrow() {
501                Ok(b) => b.is_yieldable(),
502                Err(_) => false,
503            };
504            target_is_yieldable
505        }
506    };
507    state.push(LuaValue::Bool(is_yieldable));
508    Ok(1)
509}
510
511/// `coroutine.running()` — return the current coroutine plus a boolean.
512///
513/// The boolean is `true` when the current coroutine is the main thread.
514///
515pub fn co_running(state: &mut LuaState) -> Result<usize, LuaError> {
516    // TODO(port): push_thread pushes a Thread value for the current LuaState and
517    // returns true iff it is the main thread; Phase B wire-up needed.
518    let is_main = state.push_thread()?;
519    // Lua 5.1's `coroutine.running()` returns nil in the main coroutine and only
520    // the running thread (one value) inside a coroutine — the second `is-main`
521    // boolean is a 5.2 addition. Verified against lua5.1.5:
522    // `coroutine.running()` in main prints `nil`. See
523    // specs/followup/5.1-roster-syntax.md §1.
524    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
525        if is_main {
526            state.pop_n(1);
527            state.push(LuaValue::Nil);
528        }
529        return Ok(1);
530    }
531    state.push(LuaValue::Bool(is_main));
532    Ok(2)
533}
534
535/// `coroutine.close(co)` — close a dead or suspended coroutine.
536///
537/// Closes a coroutine, running any pending to-be-closed variables via
538/// `__close` and resetting its status. Valid only when the target is
539/// suspended (`Yield`) or dead (`Ok` with no active frames).
540/// Calling on a running or normal coroutine raises an error.
541///
542pub fn co_close(state: &mut LuaState) -> Result<usize, LuaError> {
543    lua_vm::state::inc_c_stack(state)?;
544    let result = (|| {
545        let co = get_opt_co(state)?;
546        let status = aux_status(state, &co);
547        match status {
548            COS_DEAD | COS_YIELD => close_suspended_or_dead(state, co),
549            _ => {
550                if matches!(state.global().lua_version, lua_types::LuaVersion::V55)
551                    && status == COS_RUN
552                    && state.global().closing_thread_id == Some(co.id)
553                {
554                    state.push(LuaValue::Bool(true));
555                    return Ok(1);
556                }
557                if matches!(state.global().lua_version, lua_types::LuaVersion::V55)
558                    && status == COS_RUN
559                    && co.id == state.global().main_thread_id
560                {
561                    return Err(LuaError::runtime(format_args!("cannot close main thread")));
562                }
563                if matches!(state.global().lua_version, lua_types::LuaVersion::V55)
564                    && status == COS_RUN
565                    && co.id == state.global().current_thread_id
566                {
567                    state.global_mut().closing_thread_id = Some(co.id);
568                    let in_status = state.status as i32;
569                    let s = lua_vm::state::reset_thread(state, in_status);
570                    state.global_mut().closing_thread_id = None;
571                    state.n_ccalls = state.n_ccalls.saturating_sub(1);
572                    std::panic::panic_any(LuaThreadClose(LuaStatus::from_raw(s)));
573                }
574                let name = if status == COS_RUN {
575                    "running"
576                } else {
577                    "normal"
578                };
579                Err(LuaError::runtime(format_args!(
580                    "cannot close a {} coroutine",
581                    name
582                )))
583            }
584        }
585    })();
586    state.n_ccalls -= 1;
587    result
588}
589
590/// Performs the actual close for a suspended or dead coroutine.
591fn close_suspended_or_dead(
592    state: &mut LuaState,
593    co: GcRef<lua_types::value::LuaThread>,
594) -> Result<usize, LuaError> {
595    let co_id = co.id;
596    let entry_rc_opt = {
597        let g = state.global();
598        g.threads.get(&co_id).map(|e| e.state.clone())
599    };
600    let entry_rc = match entry_rc_opt {
601        Some(rc) => rc,
602        None => {
603            state.push(LuaValue::Bool(true));
604            return Ok(1);
605        }
606    };
607    let parent_thread_id = state.global().current_thread_id;
608    let caller_c_calls = state.c_calls();
609
610    let parent_open_upval_slots: Vec<(u64, lua_vm::state::StackIdx)> = state
611        .openupval
612        .iter()
613        .filter_map(|uv| match &*uv.slot() {
614            lua_types::UpValState::Open { thread_id, idx } => Some((*thread_id as u64, *idx)),
615            lua_types::UpValState::Closed(_) => None,
616        })
617        .collect();
618    {
619        let mut g = state.global_mut();
620        for (tid, idx) in &parent_open_upval_slots {
621            let val = state.get_at(*idx);
622            g.cross_thread_upvals.insert((*tid, *idx), val);
623        }
624    }
625
626    push_parent_gc_snapshot(state);
627
628    let (status, err_value): (i32, Option<LuaValue>) = {
629        let mut co_state = entry_rc.borrow_mut();
630        co_state.global_mut().current_thread_id = co_id;
631        co_state.global_mut().closing_thread_id = Some(co_id);
632        co_state.n_ccalls = caller_c_calls;
633        let in_status = co_state.status as i32;
634        let s = lua_vm::state::reset_thread(&mut *co_state, in_status);
635        co_state.global_mut().closing_thread_id = None;
636        co_state.global_mut().current_thread_id = parent_thread_id;
637        if s == LuaStatus::Ok as i32 {
638            (s, None)
639        } else {
640            let top = co_state.top_idx().0;
641            if top > 0 {
642                let err = co_state.get_at(lua_vm::state::StackIdx(top - 1));
643                co_state.set_top(lua_vm::state::StackIdx(top - 1));
644                (s, Some(err))
645            } else {
646                (s, Some(LuaValue::Nil))
647            }
648        }
649    };
650
651    pop_parent_gc_snapshot(state);
652
653    {
654        let mut g = state.global_mut();
655        let mut flush: Vec<(lua_vm::state::StackIdx, LuaValue)> = Vec::new();
656        for (tid, idx) in &parent_open_upval_slots {
657            if let Some(v) = g.cross_thread_upvals.remove(&(*tid, *idx)) {
658                flush.push((*idx, v));
659            }
660        }
661        drop(g);
662        for (idx, v) in flush {
663            state.set_at(idx, v);
664        }
665    }
666
667    if status == LuaStatus::Ok as i32 {
668        state.push(LuaValue::Bool(true));
669        Ok(1)
670    } else {
671        state.push(LuaValue::Bool(false));
672        if let Some(v) = err_value {
673            state.push(v);
674        } else {
675            state.push(LuaValue::Nil);
676        }
677        Ok(2)
678    }
679}
680
681// ── Module entry point ────────────────────────────────────────────────────────
682
683/// Opens the `coroutine` standard library by pushing a new table containing
684/// all `coroutine.*` functions.
685///
686pub fn open_coroutine(state: &mut LuaState) -> Result<usize, LuaError> {
687    // `coroutine.close` is a Lua 5.4 addition tied to to-be-closed variables
688    // (`specs/research/5.3-upstream-delta.md` delta #9). Under 5.3 it is absent
689    // from the roster entirely.
690    use lua_types::LuaVersion;
691    let version = state.global().lua_version;
692    let has_close = !matches!(version, LuaVersion::V51 | LuaVersion::V52 | LuaVersion::V53);
693    // `coroutine.isyieldable` is a Lua 5.3 addition; it is absent in 5.1 and 5.2
694    // (verified against lua5.1.5 and lua5.2.4: `type(coroutine.isyieldable)` ==
695    // "nil"). See specs/followup/5.1-roster-syntax.md §1.
696    let has_isyieldable = !matches!(version, LuaVersion::V51 | LuaVersion::V52);
697    if has_close && has_isyieldable {
698        state.new_lib(CO_FUNCS)?;
699    } else {
700        let filtered: Vec<(&[u8], lua_CFunction)> = CO_FUNCS
701            .iter()
702            .filter(|(name, _)| {
703                (has_close || *name != b"close".as_slice())
704                    && (has_isyieldable || *name != b"isyieldable".as_slice())
705            })
706            .copied()
707            .collect();
708        state.new_lib(&filtered)?;
709    }
710    Ok(1)
711}
712
713// ──────────────────────────────────────────────────────────────────────────────
714// PORT STATUS
715//   source:        src/lcorolib.c  (210 lines, 12 functions)
716//   target_crate:  lua-stdlib
717//   confidence:    medium
718//   todos:         21
719//   port_notes:    2
720//   unsafe_blocks: 0
721//   notes:         All coroutine execution primitives (resume, yield, xmove,
722//                  new_thread, close_thread) are Phase E stubs that panic.
723//                  Argument-checking / result-packaging logic is faithfully
724//                  translated so Phase E can drop in real implementations.
725//                  The CO_FUNCS table type references lua_CFunction which is
726//                  resolved in Phase B.  LuaState / GcRef<LuaState> / LuaStatus
727//                  imports are all deferred to Phase B.
728// ──────────────────────────────────────────────────────────────────────────────