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