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