Skip to main content

lua_vm/
do_.rs

1//! Stack and call structure of Lua.
2//!
3//! Translated from `src/ldo.c` (Lua 5.4.7, ~1029 lines, ~37 functions).
4//! Target crate: lua-vm (`crates/lua-vm/src/do_.rs`).
5
6// TODO(port): imports — exact module paths depend on final crate layout settled in Phase B.
7// All `use` paths below are best-guess from file_deps.txt + types.tsv.
8#[allow(unused_imports)] use crate::prelude::*;
9use crate::{
10    func,
11    state::{CallInfoIdx, LuaState},
12    vm,
13};
14use lua_types::{
15    error::LuaError,
16    status::LuaStatus,
17    value::LuaValue,
18};
19use lua_types::StackIdx;
20use lua_types::closure::LuaClosure;
21use lua_types::tagmethod::TagMethod;
22use crate::zio::{ZIO, LexBuffer};
23
24/// Stub DynData. TODO(phase-b): real type lives in lua-parse.
25struct DynDataStub;
26impl DynDataStub {
27    fn new() -> Self { DynDataStub }
28}
29
30/// Text-source parser entry point.
31///
32/// C: `LClosure *luaY_parser(lua_State *L, ZIO *z, Mbuffer *buff,
33///                            Dyndata *dyd, const char *name, int firstchar)`
34///
35/// PORT NOTE: A direct call into `lua_parse::parse` would create a cyclic
36/// crate dependency (`lua-parse` already depends on `lua-vm`). Instead the
37/// embedder installs a function pointer on `GlobalState::parser_hook` at
38/// startup; when present, this stub delegates to it. When absent (e.g. in
39/// internal unit tests that never load text), we surface a syntax error so
40/// the runtime can route it through `pcall` instead of panicking.
41fn parse_stub(
42    state: &mut LuaState,
43    z: &mut ZIO,
44    _buff: &mut LexBuffer,
45    _dyd: &mut DynDataStub,
46    name: &[u8],
47    c: i32,
48) -> Result<lua_types::GcRef<lua_types::closure::LuaLClosure>, LuaError> {
49    let hook = state.global().parser_hook;
50    if let Some(parse) = hook {
51        let mut source: Vec<u8> = Vec::new();
52        if c >= 0 {
53            source.push(c as u8);
54        }
55        loop {
56            let b = z.getc();
57            if b < 0 {
58                break;
59            }
60            source.push(b as u8);
61        }
62        return parse(state, &source, name, c);
63    }
64    Err(LuaError::syntax(format_args!(
65        "{}: Lua text parser not yet wired (phase-b: lua-parse::parse)",
66        core::str::from_utf8(name).unwrap_or("?"),
67    )))
68}
69
70// ── Constants ────────────────────────────────────────────────────────────────
71
72// C: #define ERRORSTACKSIZE (LUAI_MAXSTACK + 200)
73// PORT NOTE: LUAI_MAXSTACK is 1_000_000 per macros.tsv.
74const LUAI_MAXSTACK: usize = 1_000_000;
75const ERRORSTACKSIZE: usize = LUAI_MAXSTACK + 200;
76
77// C: const EXTRA_STACK = 5  (macros.tsv)
78const EXTRA_STACK: i32 = 5;
79
80// C: const LUA_MINSTACK = 20  (macros.tsv)
81const LUA_MINSTACK: i32 = 20;
82
83// C: const LUA_MULTRET = -1  (macros.tsv)
84const LUA_MULTRET: i32 = -1;
85
86// C: const NYCI = 0x10001  (macros.tsv: non-yieldable call increment)
87const NYCI: u32 = 0x10001;
88
89// C: #define LUAI_MAXCCALLS  — typically 200 in luaconf.h
90// TODO(port): confirm from luaconf.h or a constants module.
91const LUAI_MAXCCALLS: u32 = 200;
92
93// CallStatus bit flags (macros.tsv)
94const CIST_C: u16 = 1 << 1;
95const CIST_FRESH: u16 = 1 << 2;
96const CIST_HOOKED: u16 = 1 << 3;
97const CIST_YPCALL: u16 = 1 << 4;
98const CIST_TAIL: u16 = 1 << 5;
99const CIST_HOOKYIELD: u16 = 1 << 6;
100const CIST_TRAN: u16 = 1 << 8;
101const CIST_CLSRET: u16 = 1 << 9;
102const CIST_FIN: u16 = 1 << 7;
103
104// C: LUA_MASKCALL, LUA_MASKRET  (macros.tsv  →  hook event bitmasks)
105// TODO(port): derive from HookEvent enum once that type is settled.
106const LUA_MASKCALL: u8 = 1 << 0;
107const LUA_MASKRET: u8 = 1 << 1;
108
109// C: LUA_HOOKCALL, LUA_HOOKRET, LUA_HOOKTAILCALL event codes
110const LUA_HOOKCALL: i32 = 0;
111const LUA_HOOKRET: i32 = 1;
112const LUA_HOOKTAILCALL: i32 = 4;
113
114// C: CLOSEKTOP = -1  (macros.tsv: "close all upvals down to top" sentinel)
115// PORT NOTE: luaF_close takes StackIdx; this sentinel needs special handling.
116// TODO(port): settle representation with func.rs author.
117const CLOSE_K_TOP: i32 = -1;
118
119// ── Helper: errorstatus ──────────────────────────────────────────────────────
120
121// C: #define errorstatus(s) ((s) > LUA_YIELD)
122// LUA_OK = 0, LUA_YIELD = 1; any status > 1 is a real error.
123#[inline]
124fn error_status(s: LuaStatus) -> bool {
125    (s as i32) > (LuaStatus::Yield as i32)
126}
127
128// ── lua_longjmp (NOT translated) ─────────────────────────────────────────────
129// PORT NOTE: The `struct lua_longjmp` and the entire setjmp/longjmp mechanism
130// (LUAI_THROW / LUAI_TRY) are replaced by Rust's `Result<T, LuaError>`.
131// There is no Rust equivalent of the `lua_longjmp` struct.
132// The `lua_State.errorJmp` field is removed (see types.tsv).
133
134// ══════════════════════════════════════════════════════════════════════════════
135// Error-recovery functions
136// ══════════════════════════════════════════════════════════════════════════════
137
138/// Sets the error object at `old_top` and adjusts the stack top.
139///
140/// C: `void luaD_seterrorobj(lua_State *L, int errcode, StkId oldtop)`
141pub(crate) fn set_error_obj(state: &mut LuaState, errcode: LuaStatus, old_top: StackIdx) {
142    // C: switch (errcode)
143    match errcode {
144        LuaStatus::ErrMem => {
145            // C: setsvalue2s(L, oldtop, G(L)->memerrmsg)
146            // reuse the preallocated OOM message string
147            let memerrmsg = state.global().memerrmsg.clone();
148            state.set_at(old_top, LuaValue::Str(memerrmsg));
149        }
150        LuaStatus::ErrErr => {
151            // C: setsvalue2s(L, oldtop, luaS_newliteral(L, "error in error handling"))
152            if let Ok(s) = state.intern_str(b"error in error handling") {
153                state.set_at(old_top, LuaValue::Str(s));
154            }
155        }
156        LuaStatus::Ok => {
157            // C: setnilvalue(s2v(oldtop)) — special case only for closing upvalues
158            state.set_at(old_top, LuaValue::Nil);
159        }
160        _ => {
161            // C: lua_assert(errorstatus(errcode)); real error
162            debug_assert!(error_status(errcode));
163            // C: setobjs2s(L, oldtop, L->top.p - 1) — error message on current top
164            let top = state.top_idx();
165            let err_val = state.get_at(top - 1).clone();
166            state.set_at(old_top, err_val);
167        }
168    }
169    // C: L->top.p = oldtop + 1
170    state.set_top(old_top + 1);
171}
172
173/// Throws an error, escalating to the main thread or panicking if no handler exists.
174///
175/// C: `l_noret luaD_throw(lua_State *L, int errcode)`
176///
177/// PORT NOTE: In the Rust port, errors propagate via `Result<T, LuaError>` — callers
178/// of this function should instead write `return Err(LuaError::with_status(errcode))`.
179/// This function exists only for the rare "no handler anywhere" abort path.
180/// The `l_noret` C annotation maps to `-> !` (never type).
181pub(crate) fn throw(state: &mut LuaState, errcode: LuaStatus) -> ! {
182    // TODO(port): main-thread escalation — C copies the error object to
183    // g->mainthread and re-throws there. This requires coroutine support
184    // (Phase E). In Phase A, fall through to the panic handler.
185
186    // TODO(port): panic handler — C calls g->panic(L) if set. The panic
187    // function is a lua_CFunction; calling it requires proper API setup.
188    // For now, skip to the abort equivalent.
189
190    // C: abort()
191    // PORTING.md: std::process outside lua-cli is banned; use panic! instead.
192    panic!("luaD_throw: unhandled Lua error (status = {:?}), no error handler", errcode)
193}
194
195/// Runs `f` in a "protected" context, catching any `LuaError` it returns.
196/// Restores `nCcalls` on both success and error.
197///
198/// C: `int luaD_rawrunprotected(lua_State *L, Pfunc f, void *ud)`
199///
200/// PORT NOTE: The C implementation uses setjmp/longjmp for protection. In Rust
201/// the same protection is provided by `Result<T, LuaError>` — the function just
202/// calls `f` and returns the result. The `ud` void* argument is captured in the
203/// closure environment instead of being passed separately.
204pub(crate) fn raw_run_protected<F>(state: &mut LuaState, f: F) -> Result<(), LuaError>
205where
206    F: FnOnce(&mut LuaState) -> Result<(), LuaError>,
207{
208    // C: l_uint32 oldnCcalls = L->nCcalls;
209    let old_n_ccalls = state.nCcalls;
210    // C: LUAI_TRY(L, &lj, (*f)(L, ud));
211    // PORT NOTE: setjmp/longjmp replaced by Result; f(state) propagates errors naturally.
212    let result = f(state);
213    // C: L->errorJmp = lj.previous; L->nCcalls = oldnCcalls;
214    state.nCcalls = old_n_ccalls;
215    result
216}
217
218// ══════════════════════════════════════════════════════════════════════════════
219// Stack reallocation
220// ══════════════════════════════════════════════════════════════════════════════
221
222// PORT NOTE: `relstack` and `correctstack` from ldo.c are NOT translated.
223// In C, they convert all stack pointers to/from byte-offsets before/after
224// `realloc` (which may move the allocation). In Rust the stack is a
225// `Vec<StackValue>` and all references are `StackIdx` (u32 index) — they are
226// already position-stable across reallocation.  Nothing to save or restore.
227
228/// Reallocates the stack to `new_size` slots, filling new slots with `Nil`.
229/// Returns `Ok(true)` on success, `Ok(false)` when `raise_error` is false and
230/// the allocation fails, or `Err(LuaError::Memory)` when `raise_error` is true.
231///
232/// C: `int luaD_reallocstack(lua_State *L, int newsize, int raiseerror)`
233pub(crate) fn realloc_stack(
234    state: &mut LuaState,
235    new_size: usize,
236    raise_error: bool,
237) -> Result<bool, LuaError> {
238    // C: int oldsize = stacksize(L);
239    let old_size = state.stack_size() as usize;
240    debug_assert!(new_size <= LUAI_MAXSTACK || new_size == ERRORSTACKSIZE);
241
242    // C: int oldgcstop = G(L)->gcstopem; G(L)->gcstopem = 1;
243    // PORT NOTE: stop emergency GC during reallocation so the allocator
244    // (which may trigger GC) doesn't see a stack in mid-realloc state.
245    let old_gcstop = state.global().gcstopem;
246    state.global_mut().gcstopem = true;
247
248    // C: newstack = luaM_reallocvector(...)
249    // luaM_reallocvector → v.resize_with(n, T::default) (macros.tsv)
250    let new_extent = new_size as usize + EXTRA_STACK as usize;
251    let alloc_result = state.stack_resize(new_extent);
252
253    // C: G(L)->gcstopem = oldgcstop;
254    state.global_mut().gcstopem = old_gcstop;
255
256    if alloc_result.is_err() {
257        // C: correctstack(L) — no-op in Rust (see PORT NOTE above)
258        if raise_error {
259            // C: luaM_error(L) → return Err(LuaError::Memory)
260            return Err(LuaError::Memory);
261        } else {
262            return Ok(false);
263        }
264    }
265
266    // C: correctstack(L) — no-op in Rust
267    // C: L->stack_last.p = L->stack.p + newsize;
268    state.stack_last = StackIdx(new_size as u32);
269
270    // C: for (i = oldsize+EXTRA_STACK; i < newsize+EXTRA_STACK; i++) setnilvalue(...)
271    // Initialize newly allocated slots to Nil.
272    let old_extent = old_size + EXTRA_STACK as usize;
273    for i in old_extent..new_extent {
274        state.stack_set_nil(i);
275    }
276
277    Ok(true)
278}
279
280/// Tries to grow the stack by at least `n` elements.
281/// Returns `Ok(true)` on success, `Ok(false)` on soft failure (when
282/// `raise_error` is false), or `Err(LuaError::Runtime("stack overflow"))` when
283/// `raise_error` is true and the stack is already at maximum.
284///
285/// C: `int luaD_growstack(lua_State *L, int n, int raiseerror)`
286pub(crate) fn grow_stack(
287    state: &mut LuaState,
288    n: i32,
289    raise_error: bool,
290) -> Result<bool, LuaError> {
291    // C: int size = stacksize(L);
292    let size = state.stack_size();
293
294    // C: if (l_unlikely(size > LUAI_MAXSTACK))
295    if size > LUAI_MAXSTACK {
296        // Thread already using the error-overflow extension; cannot grow further.
297        debug_assert!(state.stack_size() == ERRORSTACKSIZE);
298        if raise_error {
299            // C: luaD_throw(L, LUA_ERRERR)
300            return Err(LuaError::with_status(LuaStatus::ErrErr));
301        }
302        return Ok(false);
303    } else if (n as usize) < LUAI_MAXSTACK {
304        // C: int newsize = 2 * size;
305        let mut new_size = 2 * size;
306        // C: int needed = cast_int(L->top.p - L->stack.p) + n;
307        let needed = (state.top_idx().0 as i32 + n) as usize;
308        if new_size > LUAI_MAXSTACK {
309            new_size = LUAI_MAXSTACK;
310        }
311        if new_size < needed {
312            new_size = needed;
313        }
314        if new_size <= LUAI_MAXSTACK {
315            return realloc_stack(state, new_size, raise_error);
316        }
317    }
318    // Stack overflow — allocate error extension so we can raise a message.
319    realloc_stack(state, ERRORSTACKSIZE, raise_error)?;
320    if raise_error {
321        // C: luaG_runerror(L, "stack overflow")
322        return Err(LuaError::runtime(format_args!("stack overflow")));
323    }
324    Ok(false)
325}
326
327/// Computes the number of stack slots currently in use across all call frames.
328///
329/// C: `static int stackinuse(lua_State *L)`
330fn stack_in_use(state: &LuaState) -> usize {
331    // C: StkId lim = L->top.p;
332    let mut lim = state.top_idx();
333    // C: for (ci = L->ci; ci != NULL; ci = ci->previous)
334    //      if (lim < ci->top.p) lim = ci->top.p;
335    let mut ci_idx_opt = Some(state.ci);
336    while let Some(ci_idx) = ci_idx_opt {
337        let ci = state.get_ci(ci_idx);
338        if lim.0 < ci.top.0 {
339            lim = ci.top;
340        }
341        ci_idx_opt = ci.previous;
342    }
343    debug_assert!(true /* TODO(phase-b): lim <= state.stack_last + EXTRA_STACK */);
344    // C: res = cast_int(lim - L->stack.p) + 1
345    let res = lim.0 as usize + 1;
346    if res < LUA_MINSTACK as usize {
347        LUA_MINSTACK as usize
348    } else {
349        res
350    }
351}
352
353/// Shrinks the stack if it is more than 3× what is currently in use.
354///
355/// C: `void luaD_shrinkstack(lua_State *L)`
356pub(crate) fn shrink_stack(state: &mut LuaState) {
357    let inuse = stack_in_use(state);
358    let max = if inuse > LUAI_MAXSTACK / 3 {
359        LUAI_MAXSTACK
360    } else {
361        inuse * 3
362    };
363    if inuse <= LUAI_MAXSTACK && state.stack_size() > max {
364        let nsize = if inuse > LUAI_MAXSTACK / 2 {
365            LUAI_MAXSTACK
366        } else {
367            inuse * 2
368        };
369        // C: luaD_reallocstack(L, nsize, 0)  — ok if that fails
370        let _ = realloc_stack(state, nsize, false);
371    }
372    // C: condmovestack(L,{},{}) — HARDSTACKTESTS only; no-op in default build (macros.tsv)
373    // C: luaE_shrinkCI(L)
374    state.shrink_ci();
375}
376
377/// Increments the stack top by one, growing the stack if necessary.
378///
379/// C: `void luaD_inctop(lua_State *L)`
380pub(crate) fn inc_top(state: &mut LuaState) -> Result<(), LuaError> {
381    // C: luaD_checkstack(L, 1)
382    // luaD_checkstack → state.check_stack(n)?  (macros.tsv)
383    state.check_stack(1)?;
384    // C: L->top.p++
385    let t = state.top_idx();
386    state.set_top(t + 1);
387    Ok(())
388}
389
390// ══════════════════════════════════════════════════════════════════════════════
391// Hook machinery
392// ══════════════════════════════════════════════════════════════════════════════
393
394/// Calls the debug hook for the given event.
395///
396/// C: `void luaD_hook(lua_State *L, int event, int line, int ftransfer, int ntransfer)`
397pub(crate) fn hook(
398    state: &mut LuaState,
399    event: i32,
400    line: i32,
401    ftransfer: i32,
402    ntransfer: i32,
403) -> Result<(), LuaError> {
404    // C: if (hook && L->allowhook)
405    if !state.has_hook() || !state.allowhook {
406        return Ok(());
407    }
408
409    let ci_idx = state.ci;
410
411    // C: ptrdiff_t top = savestack(L, L->top.p)
412    // savestack → idx  (macros.tsv: StackIdx is already an offset)
413    let saved_top = state.top_idx();
414    // C: ptrdiff_t ci_top = savestack(L, ci->top.p)
415    let saved_ci_top = state.get_ci(ci_idx).top;
416
417    let mut mask = CIST_HOOKED;
418
419    if ntransfer != 0 {
420        // C: mask |= CIST_TRAN; ci->u2.transferinfo = {ftransfer, ntransfer}
421        mask |= CIST_TRAN;
422        state.set_ci_transfer_info(ci_idx, ftransfer as u16, ntransfer as u16);
423    }
424
425    // C: if (isLua(ci) && L->top.p < ci->top.p) L->top.p = ci->top.p;
426    {
427        let ci = state.get_ci(ci_idx);
428        if ci.is_lua() {
429            let ci_top = ci.top;
430            if state.top_idx().0 < ci_top.0 {
431                state.set_top(ci_top);
432            }
433        }
434    }
435
436    // C: luaD_checkstack(L, LUA_MINSTACK)
437    state.check_stack(LUA_MINSTACK as i32)?;
438
439    // C: if (ci->top.p < L->top.p + LUA_MINSTACK) ci->top.p = L->top.p + LUA_MINSTACK;
440    {
441        let top = state.top_idx();
442        let ci = state.get_ci_mut(ci_idx);
443        if ci.top.0 < (top + LUA_MINSTACK).0 {
444            let new_top = top + LUA_MINSTACK;
445            ci.top = new_top;
446            state.clear_stack_range(top, new_top);
447        }
448    }
449
450    // C: L->allowhook = 0  — cannot call hooks inside a hook
451    state.allowhook = false;
452    state.get_ci_mut(ci_idx).callstatus |= mask;
453
454    let mut ar = crate::debug::LuaDebug::default();
455    ar.event = event;
456    ar.currentline = line;
457    ar.ftransfer = ftransfer as u16;
458    ar.ntransfer = ntransfer as u16;
459    ar.i_ci = Some(ci_idx);
460    let hook_opt = state.hook.take();
461    if let Some(mut h) = hook_opt {
462        h(state, &ar);
463        if state.hook.is_none() {
464            state.hook = Some(h);
465        }
466    }
467
468    debug_assert!(!state.allowhook);
469    state.allowhook = true;
470
471    // C: ci->top.p = restorestack(L, ci_top)
472    // restorestack → idx  (macros.tsv: StackIdx already)
473    state.get_ci_mut(ci_idx).top = saved_ci_top;
474    // C: L->top.p = restorestack(L, top)
475    state.set_top(saved_top);
476    state.get_ci_mut(ci_idx).callstatus &= !mask;
477
478    Ok(())
479}
480
481/// Executes a call hook for a Lua function entry.
482///
483/// C: `void luaD_hookcall(lua_State *L, CallInfo *ci)`
484pub(crate) fn hookcall(state: &mut LuaState, ci_idx: CallInfoIdx) -> Result<(), LuaError> {
485    // C: L->oldpc = 0
486    state.oldpc = 0;
487    if state.hookmask & LUA_MASKCALL != 0 {
488        // C: int event = (ci->callstatus & CIST_TAIL) ? LUA_HOOKTAILCALL : LUA_HOOKCALL;
489        let event = if state.get_ci(ci_idx).callstatus & CIST_TAIL != 0 {
490            LUA_HOOKTAILCALL
491        } else {
492            LUA_HOOKCALL
493        };
494        // C: Proto *p = ci_func(ci)->p;
495        // ci_func(ci) → ci.lua_closure()  (macros.tsv)
496        let numparams = {
497            // TODO(port): ci_func returns &LuaClosure::Lua; getting proto.numparams
498            // requires the full closure/proto API which isn't finalised yet.
499            state.get_ci_lua_proto_numparams(ci_idx)
500        };
501        // C: ci->u.l.savedpc++
502        let pc = state.ci_savedpc(ci_idx);
503        state.set_ci_savedpc(ci_idx, pc + 1);
504        hook(state, event, -1, 1, numparams as i32)?;
505        // C: ci->u.l.savedpc--
506        state.set_ci_savedpc(ci_idx, pc);
507    }
508    Ok(())
509}
510
511/// Executes a return hook and corrects `oldpc`.
512///
513/// C: `static void rethook(lua_State *L, CallInfo *ci, int nres)`
514fn rethook(state: &mut LuaState, ci_idx: CallInfoIdx, nres: i32) -> Result<(), LuaError> {
515    if state.hookmask & LUA_MASKRET != 0 {
516        // C: StkId firstres = L->top.p - nres
517        let first_res = state.top_idx().0 as i32 - nres;
518        let mut delta: i32 = 0;
519
520        // C: if (isLua(ci)) { Proto *p = ...; if (p->is_vararg) delta = ... }
521        if state.get_ci(ci_idx).is_lua() {
522            // TODO(port): ci_func(ci)->p accesses the Proto; needs full closure API.
523            let (is_vararg, nextraargs, numparams) =
524                state.get_ci_vararg_info(ci_idx);
525            if is_vararg {
526                // C: delta = ci->u.l.nextraargs + p->numparams + 1
527                delta = nextraargs + numparams as i32 + 1;
528            }
529        }
530
531        // C: ci->func.p += delta
532        // PORT NOTE: temporarily advance func index by delta for hook transfer calc
533        let original_func = state.get_ci(ci_idx).func;
534        state.get_ci_mut(ci_idx).func = StackIdx((original_func.0 as i32 + delta) as u32);
535
536        // C: ftransfer = cast(unsigned short, firstres - ci->func.p)
537        let ci_func = state.get_ci(ci_idx).func;
538        let ftransfer = (first_res - ci_func.0 as i32) as u16;
539
540        hook(state, LUA_HOOKRET, -1, ftransfer as i32, nres)?;
541
542        // C: ci->func.p -= delta
543        state.get_ci_mut(ci_idx).func = original_func;
544    }
545
546    // C: if (isLua(ci = ci->previous)) L->oldpc = pcRel(ci->u.l.savedpc, ci_func(ci)->p)
547    // pcRel → (pc - proto.code_base()) as i32 - 1  (macros.tsv)
548    let previous = state.get_ci(ci_idx).previous;
549    if let Some(prev_idx) = previous {
550        if state.get_ci(prev_idx).is_lua() {
551            // TODO(port): pcRel requires ci_func(ci)->p (proto code base pointer);
552            // in Rust this is a Vec<Instruction> index calculation.
553            // state.oldpc = (savedpc offset - 1) as u32
554            state.oldpc = state.get_ci_pcrel(prev_idx);
555        }
556    }
557
558    Ok(())
559}
560
561// ══════════════════════════════════════════════════════════════════════════════
562// Call mechanics
563// ══════════════════════════════════════════════════════════════════════════════
564
565/// Looks up the `__call` metamethod for `func_idx` and inserts it below
566/// the original function slot, shifting all arguments up by one.
567/// Returns the (unchanged) `func_idx` on success, or an error if no
568/// `__call` metamethod exists.
569///
570/// C: `static StkId tryfuncTM(lua_State *L, StkId func)`
571fn try_func_tm(state: &mut LuaState, func_idx: StackIdx) -> Result<StackIdx, LuaError> {
572    // C: checkstackGCp(L, 1, func)
573    // checkstackGCp → { state.check_stack(n)?; state.gc().check_step(); }  (macros.tsv)
574    // PORT NOTE: func_idx is a StackIdx and survives any stack reallocation.
575    state.check_stack(1)?;
576    state.gc_check_step();
577
578    // C: tm = luaT_gettmbyobj(L, s2v(func), TM_CALL)
579    let func_val = state.get_at(func_idx).clone();
580    let tm = state.get_tm_by_obj(&func_val, TagMethod::Call);
581
582    // C: if (l_unlikely(ttisnil(tm))) luaG_callerror(L, s2v(func))
583    if matches!(tm, LuaValue::Nil) {
584        let offender = state.get_at(func_idx).clone();
585        return Err(crate::debug::call_error(state, &offender, func_idx));
586    }
587
588    // Open a slot: shift everything from top down to func_idx up by one.
589    // C: for (p = L->top.p; p > func; p--) setobjs2s(L, p, p-1)
590    let top = state.top_idx();
591    let mut p = top;
592    while p.0 > func_idx.0 {
593        let val = state.get_at(p - 1).clone();
594        state.set_at(p, val);
595        p = p - 1;
596    }
597    // C: L->top.p++
598    state.set_top(top + 1);
599    // C: setobj2s(L, func, tm)
600    state.set_at(func_idx, tm);
601
602    Ok(func_idx)
603}
604
605/// Moves `nres` results from their current position on the stack to `res_idx`,
606/// padding with `Nil` if fewer than `wanted` results are present, or discarding
607/// extras if more are present.
608///
609/// C: `l_sinline void moveresults(lua_State *L, StkId res, int nres, int wanted)`
610#[inline(always)]
611fn move_results(
612    state: &mut LuaState,
613    res_idx: StackIdx,
614    nres: i32,
615    wanted: i32,
616) -> Result<(), LuaError> {
617    // C: switch (wanted) — handle common cases separately
618    match wanted {
619        0 => {
620            // C: L->top.p = res; return;
621            state.set_top(res_idx);
622            return Ok(());
623        }
624        1 => {
625            if nres == 0 {
626                // C: setnilvalue(s2v(res))
627                state.set_at(res_idx, LuaValue::Nil);
628            } else {
629                // C: setobjs2s(L, res, L->top.p - nres)
630                let top = state.top_idx();
631                let src = state.get_at(top - nres as i32).clone();
632                state.set_at(res_idx, src);
633            }
634            // C: L->top.p = res + 1
635            state.set_top(res_idx + 1);
636            return Ok(());
637        }
638        LUA_MULTRET => {
639            // wanted = nres: fall through to generic case below
640        }
641        _ => {
642            // C: if (hastocloseCfunc(wanted))
643            // hastocloseCfunc → n < LUA_MULTRET  (macros.tsv)
644            if wanted < LUA_MULTRET {
645                let ci_idx = state.ci;
646                // C: L->ci->callstatus |= CIST_CLSRET; L->ci->u2.nres = nres;
647                state.get_ci_mut(ci_idx).callstatus |= CIST_CLSRET;
648                state.set_ci_u2_nres(ci_idx, nres);
649
650                // C: res = luaF_close(L, res, CLOSEKTOP, 1)
651                // TODO(port): CLOSE_K_TOP sentinel needs proper StackIdx encoding
652                // in func::close; for now pass as a special sentinel value.
653                let res_idx = func::close(state, res_idx, CLOSE_K_TOP, true)?;
654
655                let ci_idx = state.ci;
656                // C: L->ci->callstatus &= ~CIST_CLSRET
657                state.get_ci_mut(ci_idx).callstatus &= !CIST_CLSRET;
658
659                if state.hookmask != 0 {
660                    // C: ptrdiff_t savedres = savestack(L, res)
661                    // savestack → idx  (macros.tsv: StackIdx is already stable)
662                    let saved_res = res_idx;
663                    rethook(state, ci_idx, nres)?;
664                    // C: res = restorestack(L, savedres) — already stable
665                    let _ = saved_res; // = res_idx (no-op restore)
666                }
667
668                // C: wanted = decodeNresults(wanted)
669                // decodeNresults → -(n) - 3  (macros.tsv)
670                let decoded_wanted = -(wanted) - 3;
671                let wanted = if decoded_wanted == LUA_MULTRET {
672                    nres
673                } else {
674                    decoded_wanted
675                };
676
677                // Fall into generic case with updated wanted.
678                let first_result = state.top_idx().0 as i32 - nres;
679                let actual_nres = nres.min(wanted);
680                for i in 0..actual_nres {
681                    let src = state.get_at((first_result + i) as u32).clone();
682                    state.set_at(res_idx + i as i32, src);
683                }
684                for i in actual_nres..wanted {
685                    state.set_at(res_idx + i as i32, LuaValue::Nil);
686                }
687                state.set_top(res_idx + wanted as i32);
688                return Ok(());
689            }
690        }
691    }
692
693    // Generic case (also reached from LUA_MULTRET with wanted = nres).
694    let effective_wanted = if wanted == LUA_MULTRET { nres } else { wanted };
695    let first_result = state.top_idx().0 as i32 - nres;
696    let actual_nres = nres.min(effective_wanted);
697    for i in 0..actual_nres {
698        let src = state.get_at((first_result + i) as u32).clone();
699        state.set_at(res_idx + i as i32, src);
700    }
701    for i in actual_nres..effective_wanted {
702        state.set_at(res_idx + i as i32, LuaValue::Nil);
703    }
704    state.set_top(res_idx + effective_wanted as i32);
705    Ok(())
706}
707
708/// Finishes a function call: calls hook if needed, moves results into place,
709/// and pops the current call frame.
710///
711/// C: `void luaD_poscall(lua_State *L, CallInfo *ci, int nres)`
712#[inline(always)]
713pub(crate) fn poscall(
714    state: &mut LuaState,
715    ci_idx: CallInfoIdx,
716    nres: i32,
717) -> Result<(), LuaError> {
718    // C: int wanted = ci->nresults;
719    let wanted = state.get_ci(ci_idx).nresults as i32;
720
721    // C: if (l_unlikely(L->hookmask && !hastocloseCfunc(wanted))) rethook(L, ci, nres);
722    if state.hookmask != 0 && !(wanted < LUA_MULTRET) {
723        rethook(state, ci_idx, nres)?;
724    }
725
726    // C: moveresults(L, ci->func.p, nres, wanted)
727    let func_idx = state.get_ci(ci_idx).func;
728    move_results(state, func_idx, nres, wanted)?;
729
730    // C: lua_assert(!(ci->callstatus & (CIST_HOOKED|CIST_YPCALL|CIST_FIN|CIST_TRAN|CIST_CLSRET)))
731    debug_assert!(
732        state.get_ci(ci_idx).callstatus
733            & (CIST_HOOKED | CIST_YPCALL | CIST_FIN | CIST_TRAN | CIST_CLSRET)
734            == 0
735    );
736
737    // C: L->ci = ci->previous
738    let previous = state
739        .get_ci(ci_idx)
740        .previous
741        .expect("poscall: no previous call frame");
742    state.ci = previous;
743    Ok(())
744}
745
746/// Advances to the next `CallInfo` slot, allocating a new one if required.
747/// Sets `state.ci` to the new frame and fills its fields.
748///
749/// C: `l_sinline CallInfo *prepCallInfo(lua_State *L, StkId func, int nret, int mask, StkId top)`
750#[inline(always)]
751fn prep_call_info(
752    state: &mut LuaState,
753    func_idx: StackIdx,
754    nret: i32,
755    mask: u16,
756    top_idx: StackIdx,
757) -> Result<CallInfoIdx, LuaError> {
758    // C: CallInfo *ci = L->ci = next_ci(L)
759    // next_ci → L->ci->next ? L->ci->next : luaE_extendCI(L)
760    let ci_idx = state.next_ci()?;
761    state.ci = ci_idx;
762    {
763        let ci = state.get_ci_mut(ci_idx);
764        ci.func = func_idx;
765        ci.nresults = nret as i16;
766        ci.callstatus = mask;
767        ci.top = top_idx;
768        ci.u = if (mask & crate::state::CIST_C) != 0 {
769            crate::state::CallInfoFrame::c_default()
770        } else {
771            crate::state::CallInfoFrame::lua_default()
772        };
773    }
774    Ok(ci_idx)
775}
776
777/// Pre-call for C functions: sets up a CallInfo, fires the call hook if needed,
778/// invokes the C function, and calls `poscall`.
779/// Returns the number of values returned by the C function.
780///
781/// C: `l_sinline int precallC(lua_State *L, StkId func, int nresults, lua_CFunction f)`
782#[inline(always)]
783fn precall_c(
784    state: &mut LuaState,
785    func_idx: StackIdx,
786    nresults: i32,
787    f: crate::state::LuaCFunction,
788) -> Result<i32, LuaError> {
789    // C: checkstackGCp(L, LUA_MINSTACK, func)
790    state.check_stack(LUA_MINSTACK as i32)?;
791    state.gc_check_step();
792
793    let top_idx = state.top_idx();
794    // C: L->ci = ci = prepCallInfo(L, func, nresults, CIST_C, L->top.p + LUA_MINSTACK)
795    let ci_idx = prep_call_info(state, func_idx, nresults, CIST_C, top_idx + LUA_MINSTACK)?;
796
797    // C: lua_assert(ci->top.p <= L->stack_last.p)
798    debug_assert!(true /* TODO(phase-b): state.get_ci(ci_idx).top <= state.stack_last */);
799
800    // C: if (l_unlikely(L->hookmask & LUA_MASKCALL))
801    if state.hookmask & LUA_MASKCALL != 0 {
802        // C: int narg = cast_int(L->top.p - func) - 1
803        let narg = (state.top_idx().0 as i32 - func_idx.0 as i32) - 1;
804        hook(state, LUA_HOOKCALL, -1, 1, narg)?;
805    }
806
807    // C: lua_unlock(L) — no-op (macros.tsv)
808    // C: n = (*f)(L)
809    let n = f(state)? as i32;
810    // C: lua_lock(L) — no-op (macros.tsv)
811
812    // C: api_checknelems(L, n)
813    // api_checknelems → debug_assert!(n < (top - ci_func), "not enough elements") (macros.tsv)
814    debug_assert!(
815        n <= state.top_idx().0 as i32,
816        "C function returned more values than available"
817    );
818
819    poscall(state, ci_idx, n)?;
820    Ok(n)
821}
822
823/// Prepares a tail call, reusing the current `CallInfo`.
824/// Returns the result count for C functions, or `-1` to signal the VM that a
825/// Lua function should continue executing.
826///
827/// C: `int luaD_pretailcall(lua_State *L, CallInfo *ci, StkId func, int narg1, int delta)`
828pub(crate) fn pretailcall(
829    state: &mut LuaState,
830    ci_idx: CallInfoIdx,
831    mut func_idx: StackIdx,
832    mut narg1: i32,
833    delta: i32,
834) -> Result<i32, LuaError> {
835    // C: retry: switch (ttypetag(s2v(func))) { ... default: goto retry; }
836    loop {
837        let func_val = state.get_at(func_idx).clone();
838        match func_val {
839            // C: case LUA_VCCL — return precallC(L, func, LUA_MULTRET, clCvalue(s2v(func))->f);
840            LuaValue::Function(LuaClosure::C(ref cl)) => {
841                let cfunc = state.global().c_functions[cl.func];
842                return precall_c(state, func_idx, LUA_MULTRET, cfunc);
843            }
844            // C: case LUA_VLCF — return precallC(L, func, LUA_MULTRET, fvalue(s2v(func)));
845            LuaValue::Function(LuaClosure::LightC(f)) => {
846                let cfunc = state.global().c_functions[f];
847                return precall_c(state, func_idx, LUA_MULTRET, cfunc);
848            }
849            // C: case LUA_VLCL — Lua function
850            LuaValue::Function(LuaClosure::Lua(ref cl)) => {
851                let proto = cl.proto.clone();
852                let fsize = proto.maxstacksize as i32;
853                let nfixparams = proto.numparams as i32;
854
855                // C: checkstackGCp(L, fsize - delta, func)
856                state.check_stack(fsize - delta)?;
857                state.gc_check_step();
858
859                // C: ci->func.p -= delta  (restore 'func' if vararg)
860                {
861                    let ci = state.get_ci_mut(ci_idx);
862                    ci.func = StackIdx((ci.func.0 as i32 - delta) as u32);
863                }
864                let ci_func = state.get_ci(ci_idx).func;
865
866                // C: for (i = 0; i < narg1; i++) setobjs2s(L, ci->func.p + i, func + i)
867                for i in 0..narg1 {
868                    let src = state.get_at(func_idx + i as i32).clone();
869                    state.set_at(ci_func + i as i32, src);
870                }
871
872                // Update func_idx to reflect the moved-down position.
873                func_idx = ci_func;
874
875                // C: for (; narg1 <= nfixparams; narg1++) setnilvalue(s2v(func+narg1))
876                while narg1 <= nfixparams {
877                    state.set_at(func_idx + narg1 as i32, LuaValue::Nil);
878                    narg1 += 1;
879                }
880
881                // C: ci->top.p = func + 1 + fsize
882                {
883                    let new_ci_top = func_idx + 1 + fsize as i32;
884                    let stack_last = state.stack_last;
885                    let live_top = state.top_idx();
886                    let ci = state.get_ci_mut(ci_idx);
887                    ci.top = new_ci_top;
888                    debug_assert!(ci.top.0 <= stack_last.0);
889                    // C: ci->u.l.savedpc = p->code  (starting point — offset 0)
890                    ci.set_saved_pc(0);
891                    ci.callstatus |= CIST_TAIL;
892                    state.clear_stack_range(live_top, new_ci_top);
893                }
894
895                // C: L->top.p = func + narg1
896                state.set_top(func_idx + narg1 as i32);
897                return Ok(-1); // Signal: Lua function, VM should continue.
898            }
899            _ => {
900                // C: default: func = tryfuncTM(L, func); narg1++; goto retry;
901                func_idx = try_func_tm(state, func_idx)?;
902                narg1 += 1;
903                // continue the loop — equivalent to goto retry
904            }
905        }
906    }
907}
908
909/// Prepares a call to `func_idx` (C or Lua).
910/// For C functions, also executes the call and returns `None`.
911/// For Lua functions, returns `Some(ci_idx)` — the caller must then invoke the VM.
912///
913/// C: `CallInfo *luaD_precall(lua_State *L, StkId func, int nresults)`
914///
915/// PORT NOTE (perf): the C source uses `retry: switch (...) { default: goto retry; }`.
916/// We split that into a fast-path call to the Lua-closure handler and an explicit
917/// retry loop for the rare metamethod miss-path. The fast path inlines the Lua-closure
918/// arm so LLVM can specialize for the by-far-most-common case (a direct Lua call).
919#[inline(always)]
920pub(crate) fn precall(
921    state: &mut LuaState,
922    func_idx: StackIdx,
923    nresults: i32,
924) -> Result<Option<CallInfoIdx>, LuaError> {
925    if let LuaValue::Function(LuaClosure::Lua(cl)) =
926        &state.stack[func_idx.0 as usize].val
927    {
928        let nfixparams = cl.proto.numparams as i32;
929        let fsize = cl.proto.maxstacksize as i32;
930        let narg = (state.top_idx().0 as i32 - func_idx.0 as i32) - 1;
931
932        state.check_stack(fsize)?;
933        state.gc_check_step();
934
935        let ci_idx =
936            prep_call_info(state, func_idx, nresults, 0, func_idx + 1 + fsize as i32)?;
937        state.set_ci_savedpc(ci_idx, 0);
938
939        if narg < nfixparams {
940            fill_missing_params(state, narg, nfixparams);
941        }
942        return Ok(Some(ci_idx));
943    }
944    precall_slow(state, func_idx, nresults)
945}
946
947/// Cold path: fills `nfixparams - narg` nil values onto the stack.
948///
949/// C: `for (; narg < nfixparams; narg++) setnilvalue(s2v(L->top.p++))`
950/// (the body of the loop in `luaD_precall`).
951#[cold]
952#[inline(never)]
953fn fill_missing_params(state: &mut LuaState, mut narg: i32, nfixparams: i32) {
954    while narg < nfixparams {
955        let top = state.top_idx();
956        state.set_at(top, LuaValue::Nil);
957        state.set_top(top + 1);
958        narg += 1;
959    }
960}
961
962/// Cold path: callee is a C closure, light C function, or a non-function with
963/// a `__call` metamethod. Mirrors the structure of C-Lua's `retry:` loop in
964/// `luaD_precall`.
965#[cold]
966#[inline(never)]
967fn precall_slow(
968    state: &mut LuaState,
969    mut func_idx: StackIdx,
970    nresults: i32,
971) -> Result<Option<CallInfoIdx>, LuaError> {
972    loop {
973        let func_val = state.get_at(func_idx).clone();
974        match func_val {
975            LuaValue::Function(LuaClosure::C(ref cl)) => {
976                let cfunc = state.global().c_functions[cl.func];
977                precall_c(state, func_idx, nresults, cfunc)?;
978                return Ok(None);
979            }
980            LuaValue::Function(LuaClosure::LightC(f)) => {
981                state.check_stack(LUA_MINSTACK as i32)?;
982                state.gc_check_step();
983
984                let top_idx = state.top_idx();
985                let ci_idx =
986                    prep_call_info(state, func_idx, nresults, CIST_C, top_idx + LUA_MINSTACK)?;
987
988                if state.hookmask & LUA_MASKCALL != 0 {
989                    let narg = (state.top_idx().0 as i32 - func_idx.0 as i32) - 1;
990                    hook(state, LUA_HOOKCALL, -1, 1, narg)?;
991                }
992
993                let cfunc = state.global().c_functions[f];
994                let n = cfunc(state)? as i32;
995                debug_assert!(
996                    n <= state.top_idx().0 as i32,
997                    "C function returned more values than available"
998                );
999                poscall(state, ci_idx, n)?;
1000                return Ok(None);
1001            }
1002            LuaValue::Function(LuaClosure::Lua(ref cl)) => {
1003                let narg = (state.top_idx().0 as i32 - func_idx.0 as i32) - 1;
1004                let nfixparams = cl.proto.numparams as i32;
1005                let fsize = cl.proto.maxstacksize as i32;
1006
1007                state.check_stack(fsize)?;
1008                state.gc_check_step();
1009
1010                let ci_idx = prep_call_info(
1011                    state,
1012                    func_idx,
1013                    nresults,
1014                    0,
1015                    func_idx + 1 + fsize as i32,
1016                )?;
1017                state.set_ci_savedpc(ci_idx, 0);
1018
1019                if narg < nfixparams {
1020                    fill_missing_params(state, narg, nfixparams);
1021                }
1022                return Ok(Some(ci_idx));
1023            }
1024            _ => {
1025                func_idx = try_func_tm(state, func_idx)?;
1026            }
1027        }
1028    }
1029}
1030
1031/// Internal call helper shared by `call` and `callnoyield`.
1032/// `inc` is added to/subtracted from `nCcalls` around the call.
1033///
1034/// C: `l_sinline void ccall(lua_State *L, StkId func, int nResults, l_uint32 inc)`
1035#[inline]
1036fn ccall_inner(
1037    state: &mut LuaState,
1038    func_idx: StackIdx,
1039    n_results: i32,
1040    inc: u32,
1041) -> Result<(), LuaError> {
1042    ccall_inner_with_status(state, func_idx, n_results, inc, 0)
1043}
1044
1045#[inline]
1046fn ccall_inner_with_status(
1047    state: &mut LuaState,
1048    func_idx: StackIdx,
1049    n_results: i32,
1050    inc: u32,
1051    extra_callstatus: u16,
1052) -> Result<(), LuaError> {
1053    // C: L->nCcalls += inc;
1054    state.nCcalls += inc;
1055
1056    // C: if (l_unlikely(getCcalls(L) >= LUAI_MAXCCALLS))
1057    // getCcalls → state.c_calls()  (macros.tsv: lower 16 bits of nCcalls)
1058    if state.c_calls() >= LUAI_MAXCCALLS {
1059        // C: checkstackp(L, 0, func) — free any use of EXTRA_STACK
1060        // checkstackp → state.check_stack(n)?  (macros.tsv)
1061        state.check_stack(0)?;
1062        // C: luaE_checkcstack(L)
1063        state.check_c_stack()?;
1064    }
1065
1066    // C: if ((ci = luaD_precall(L, func, nResults)) != NULL)
1067    if let Some(ci_idx) = precall(state, func_idx, n_results)? {
1068        // C: ci->callstatus = CIST_FRESH; luaV_execute(L, ci);
1069        state.get_ci_mut(ci_idx).callstatus = CIST_FRESH | extra_callstatus;
1070        vm::execute(state, ci_idx)?;
1071    }
1072
1073    // C: L->nCcalls -= inc;
1074    state.nCcalls -= inc;
1075    Ok(())
1076}
1077
1078/// Calls a function through C with one recursive-invocation increment.
1079///
1080/// C: `void luaD_call(lua_State *L, StkId func, int nResults)`
1081pub(crate) fn call(
1082    state: &mut LuaState,
1083    func_idx: StackIdx,
1084    n_results: i32,
1085) -> Result<(), LuaError> {
1086    // C: ccall(L, func, nResults, 1)
1087    ccall_inner(state, func_idx, n_results, 1)
1088}
1089
1090/// Like `call` but increments the non-yieldable counter as well.
1091///
1092/// C: `void luaD_callnoyield(lua_State *L, StkId func, int nResults)`
1093pub(crate) fn callnoyield(
1094    state: &mut LuaState,
1095    func_idx: StackIdx,
1096    n_results: i32,
1097) -> Result<(), LuaError> {
1098    // C: ccall(L, func, nResults, nyci)
1099    // NYCI = 0x10001 increments both the recursion count and the non-yieldable count.
1100    ccall_inner(state, func_idx, n_results, NYCI)
1101}
1102
1103// ══════════════════════════════════════════════════════════════════════════════
1104// Yield / coroutine continuation machinery
1105// ══════════════════════════════════════════════════════════════════════════════
1106
1107/// Finishes the job of `lua_pcallk` after it was interrupted by a yield.
1108///
1109/// C: `static int finishpcallk(lua_State *L, CallInfo *ci)`
1110fn finish_pcallk(state: &mut LuaState, ci_idx: CallInfoIdx) -> Result<LuaStatus, LuaError> {
1111    // C: int status = getcistrecst(ci)
1112    // getcistrecst → ci.recover_status()  (macros.tsv)
1113    // PORT NOTE: recover_status() returns i32; convert to LuaStatus for type safety.
1114    let mut status = LuaStatus::from_raw(state.get_ci(ci_idx).recover_status());
1115
1116    if status == LuaStatus::Ok {
1117        // C: status = LUA_YIELD — was interrupted by a yield
1118        status = LuaStatus::Yield;
1119    } else {
1120        // C: StkId func = restorestack(L, ci->u2.funcidx)
1121        let func_idx = StackIdx(state.get_ci_u2_funcidx(ci_idx) as u32);
1122        // C: L->allowhook = getoah(ci->callstatus)
1123        // getoah → ci.get_oah()  (macros.tsv)
1124        state.allowhook = state.get_ci(ci_idx).get_oah();
1125        // C: func = luaF_close(L, func, status, 1)  — can yield or raise
1126        // TODO(port): CLOSE_K_TOP sentinel encoding; see close_tbc comment above.
1127        let _func_idx = func::close(state, func_idx, status as i32, true)?;
1128        // C: luaD_seterrorobj(L, status, func)
1129        set_error_obj(state, status, func_idx);
1130
1131        // PORT NOTE: lua-c invokes the message handler at error-raise time via
1132        // `luaG_errormsg`, BEFORE the longjmp propagates the error. Our error
1133        // propagation rides on Rust `Result::Err` and has no equivalent
1134        // chokepoint at raise time, so we run the handler here at the
1135        // recover/catch site — semantically equivalent. Only fires on the
1136        // yield-then-error path (the sync-error path in `pcall_k`/api.rs
1137        // calls the handler inline and clears CIST_YPCALL before we'd reach
1138        // this function). Fixes coroutine.lua:319 (xpcall + yield + error).
1139        if state.errfunc != 0 && error_status(status) && status != LuaStatus::ErrErr {
1140            let errfunc_stk = StackIdx(state.errfunc as u32);
1141            // Mirror the stack manipulation lua-c does in luaG_errormsg
1142            // (and the inline path in pcall_k api.rs:1944):
1143            //   stack: [..., err]  (top = func_idx + 1, err at func_idx)
1144            //   -> push duplicate of err -> [..., err, err]
1145            //   -> overwrite the first err slot with handler -> [..., handler, err]
1146            //   -> call_no_yield(handler_pos, 1 result) -> [..., result]
1147            //   -> result lands at func_idx, which is where the error was.
1148            let err_val = state.get_at(func_idx);
1149            state.push(err_val);
1150            let handler = state.get_at(errfunc_stk);
1151            state.set_at(state.top_idx() - 2, handler);
1152            if let Err(_) = state.call_no_yield(state.top_idx() - 2, 1) {
1153                status = LuaStatus::ErrErr;
1154                if let Ok(s) = state.intern_str(b"error in error handling") {
1155                    state.set_at(func_idx, lua_types::value::LuaValue::Str(s));
1156                }
1157                state.set_top(func_idx + 1);
1158            }
1159        }
1160
1161        // C: luaD_shrinkstack(L)
1162        shrink_stack(state);
1163        // C: setcistrecst(ci, LUA_OK)
1164        state.get_ci_mut(ci_idx).set_recover_status(LuaStatus::Ok as i32);
1165    }
1166
1167    // C: ci->callstatus &= ~CIST_YPCALL
1168    state.get_ci_mut(ci_idx).callstatus &= !CIST_YPCALL;
1169    // C: L->errfunc = ci->u.c.old_errfunc
1170    let old_errfunc = state.get_ci(ci_idx).u_c_old_errfunc();
1171    state.errfunc = old_errfunc;
1172
1173    Ok(status)
1174}
1175
1176/// Completes the execution of a C function that was interrupted by a yield.
1177///
1178/// C: `static void finishCcall(lua_State *L, CallInfo *ci)`
1179fn finish_ccall(state: &mut LuaState, ci_idx: CallInfoIdx) -> Result<(), LuaError> {
1180    let n;
1181
1182    // C: if (ci->callstatus & CIST_CLSRET)
1183    if state.get_ci(ci_idx).callstatus & CIST_CLSRET != 0 {
1184        // C: lua_assert(hastocloseCfunc(ci->nresults))
1185        debug_assert!((state.get_ci(ci_idx).nresults as i32) < LUA_MULTRET);
1186        // C: n = ci->u2.nres  — just redo luaD_poscall
1187        n = state.get_ci_u2_nres(ci_idx);
1188    } else {
1189        // C: lua_assert(ci->u.c.k != NULL && yieldable(L))
1190        debug_assert!(
1191            state.get_ci(ci_idx).u_c_k().is_some() && state.is_yieldable(),
1192            "finishCcall: no continuation or non-yieldable"
1193        );
1194
1195        let mut status = LuaStatus::Yield;
1196
1197        // C: if (ci->callstatus & CIST_YPCALL) status = finishpcallk(L, ci)
1198        if state.get_ci(ci_idx).callstatus & CIST_YPCALL != 0 {
1199            status = finish_pcallk(state, ci_idx)?;
1200        }
1201
1202        // C: adjustresults(L, LUA_MULTRET)
1203        // adjustresults → state.adjust_results(nres)  (macros.tsv)
1204        state.adjust_results(LUA_MULTRET);
1205
1206        // C: lua_unlock(L) — no-op
1207        // C: n = (*ci->u.c.k)(L, status, ci->u.c.ctx)
1208        // TODO(port): calling the continuation function while holding &mut LuaState
1209        // has the same borrow problem as the hook call. Phase E must solve this.
1210        // For now, extract and re-insert the continuation.
1211        let k = state.get_ci(ci_idx).u_c_k();
1212        let ctx = state.get_ci(ci_idx).u_c_ctx();
1213        if let Some(k_fn) = k {
1214            n = k_fn(state, status as i32, ctx)? as i32;
1215        } else {
1216            // TODO(port): unreachable in correct code; the assert above guards this
1217            return Err(LuaError::runtime(format_args!("finishCcall: missing continuation")));
1218        }
1219        // C: lua_lock(L) — no-op
1220        debug_assert!(
1221            n <= state.top_idx().0 as i32,
1222            "continuation returned more values than available"
1223        );
1224    }
1225
1226    // C: luaD_poscall(L, ci, n)
1227    poscall(state, ci_idx, n)?;
1228    Ok(())
1229}
1230
1231/// Unrolls the full continuation stack of a coroutine until empty.
1232///
1233/// C: `static void unroll(lua_State *L, void *ud)`
1234fn unroll(state: &mut LuaState) -> Result<(), LuaError> {
1235    // C: while ((ci = L->ci) != &L->base_ci)
1236    loop {
1237        let ci_idx = state.ci;
1238        if state.is_base_ci(ci_idx) {
1239            break;
1240        }
1241        if !state.get_ci(ci_idx).is_lua() {
1242            // C: finishCcall(L, ci)
1243            finish_ccall(state, ci_idx)?;
1244        } else {
1245            // C: luaV_finishOp(L); luaV_execute(L, ci);
1246            vm::finish_op(state)?;
1247            vm::execute(state, ci_idx)?;
1248        }
1249    }
1250    Ok(())
1251}
1252
1253/// Searches the call stack for the innermost suspended protected call.
1254///
1255/// C: `static CallInfo *findpcall(lua_State *L)`
1256fn find_pcall(state: &LuaState) -> Option<CallInfoIdx> {
1257    let mut ci_idx_opt = Some(state.ci);
1258    while let Some(ci_idx) = ci_idx_opt {
1259        let ci = state.get_ci(ci_idx);
1260        if ci.callstatus & CIST_YPCALL != 0 {
1261            return Some(ci_idx);
1262        }
1263        ci_idx_opt = ci.previous;
1264    }
1265    None
1266}
1267
1268/// Signals an error in the `lua_resume` call itself (not in the coroutine body).
1269///
1270/// C: `static int resume_error(lua_State *L, const char *msg, int narg)`
1271fn resume_error(state: &mut LuaState, msg: &[u8], narg: i32) -> LuaStatus {
1272    // C: L->top.p -= narg  — discard args
1273    let top = state.top_idx();
1274    state.set_top(top - narg as i32);
1275    // C: setsvalue2s(L, L->top.p, luaS_new(L, msg))
1276    // luaS_new → state.intern_str(s)  (macros.tsv)
1277    let s = state.intern_str(msg).ok();
1278    let new_top = state.top_idx();
1279    if let Some(s) = s { state.set_at(new_top, LuaValue::Str(s)); }
1280    // C: api_incr_top(L) — api_incr_top dropped; state.push() increments  (macros.tsv)
1281    state.set_top(new_top + 1);
1282    // C: lua_unlock(L) — no-op
1283    LuaStatus::ErrRun
1284}
1285
1286/// Core coroutine resume logic (runs inside `raw_run_protected`).
1287///
1288/// C: `static void resume(lua_State *L, void *ud)`
1289fn resume_coroutine(state: &mut LuaState, nargs: i32) -> Result<(), LuaError> {
1290    // C: StkId firstArg = L->top.p - n
1291    let top = state.top_idx();
1292    let first_arg = top - nargs as i32;
1293    let ci_idx = state.ci;
1294
1295    if state.status == LuaStatus::Ok as u8 {
1296        // C: ccall(L, firstArg - 1, LUA_MULTRET, 0)  — start the coroutine body
1297        ccall_inner(state, first_arg - 1, LUA_MULTRET, 0)?;
1298    } else {
1299        // C: lua_assert(L->status == LUA_YIELD); L->status = LUA_OK;
1300        debug_assert!(state.status == LuaStatus::Yield as u8);
1301        state.status = LuaStatus::Ok as u8;
1302
1303        if state.get_ci(ci_idx).is_lua() {
1304            // C: yielded inside a hook — undo savedpc increment from luaG_traceexec
1305            debug_assert!(state.get_ci(ci_idx).callstatus & CIST_HOOKYIELD != 0);
1306            let pc = state.ci_savedpc(ci_idx);
1307            state.set_ci_savedpc(ci_idx, pc.saturating_sub(1));
1308            // C: L->top.p = firstArg  — discard arguments
1309            state.set_top(first_arg);
1310            // C: luaV_execute(L, ci)
1311            vm::execute(state, ci_idx)?;
1312        } else {
1313            // C: "common" yield
1314            if let Some(k_fn) = state.get_ci(ci_idx).u_c_k() {
1315                let ctx = state.get_ci(ci_idx).u_c_ctx();
1316                // C: lua_unlock(L) — no-op
1317                // C: n = (*ci->u.c.k)(L, LUA_YIELD, ci->u.c.ctx)
1318                let n = k_fn(state, LuaStatus::Yield as i32, ctx)? as i32;
1319                // C: lua_lock(L) — no-op
1320                debug_assert!(n <= state.top_idx().0 as i32);
1321                // C: luaD_poscall(L, ci, n)
1322                poscall(state, ci_idx, n)?;
1323            } else {
1324                // No continuation: just finish the call
1325                let n = (state.top_idx().0 as i32 - first_arg.0 as i32).max(0);
1326                poscall(state, ci_idx, n)?;
1327            }
1328        }
1329
1330        // C: unroll(L, NULL)
1331        unroll(state)?;
1332    }
1333    Ok(())
1334}
1335
1336/// Unrolls the coroutine while there are recoverable (protected-call) errors.
1337///
1338/// C: `static int precover(lua_State *L, int status)`
1339fn precover(state: &mut LuaState, mut status: LuaStatus) -> LuaStatus {
1340    // C: while (errorstatus(status) && (ci = findpcall(L)) != NULL)
1341    while error_status(status) {
1342        if let Some(ci_idx) = find_pcall(state) {
1343            // C: L->ci = ci; setcistrecst(ci, status)
1344            state.ci = ci_idx;
1345            state.get_ci_mut(ci_idx).set_recover_status(status as i32);
1346            // C: status = luaD_rawrunprotected(L, unroll, NULL)
1347            // PORT NOTE: In C, luaD_throw pushes the error value onto L->top before
1348            // longjmp, so the catch in luaD_rawrunprotected leaves it there for
1349            // finish_pcallk's seterrorobj to read at L->top-1. In Rust the value
1350            // rides inside LuaError; push it explicitly to mirror the C invariant.
1351            status = match raw_run_protected(state, |s| unroll(s)) {
1352                Ok(()) => LuaStatus::Ok,
1353                Err(e) => {
1354                    let s = e.to_status();
1355                    if error_status(s) {
1356                        state.push(e.into_value());
1357                    }
1358                    s
1359                }
1360            };
1361        } else {
1362            break;
1363        }
1364    }
1365    status
1366}
1367
1368/// Resumes (or starts) a coroutine thread.
1369///
1370/// C: `LUA_API int lua_resume(lua_State *L, lua_State *from, int nargs, int *nresults)`
1371pub fn lua_resume(
1372    state: &mut LuaState,
1373    from: Option<&mut LuaState>,
1374    nargs: i32,
1375    nresults: &mut i32,
1376) -> LuaStatus {
1377    // TODO(port): coroutine support (Phase E). The implementation below is a
1378    // faithful translation of the C logic but will not work correctly until
1379    // coroutine stack switching is available. Phase A: translate the logic;
1380    // Phase E: make it actually work.
1381
1382    // C: lua_lock(L) — no-op
1383    if state.status == LuaStatus::Ok as u8 {
1384        // C: if (L->ci != &L->base_ci) — starting check
1385        if !state.is_base_ci(state.ci) {
1386            return resume_error(state, b"cannot resume non-suspended coroutine", nargs);
1387        }
1388        // C: else if (L->top.p - (L->ci->func.p + 1) == nargs) — no function?
1389        let ci_func = state.get_ci(state.ci).func;
1390        if state.top_idx().0 as i32 - (ci_func.0 as i32 + 1) == nargs {
1391            return resume_error(state, b"cannot resume dead coroutine", nargs);
1392        }
1393    } else if state.status != LuaStatus::Yield as u8 {
1394        return resume_error(state, b"cannot resume dead coroutine", nargs);
1395    }
1396
1397    // C: L->nCcalls = (from) ? getCcalls(from) : 0;
1398    state.nCcalls = from
1399        .as_ref()
1400        .map(|f| f.c_calls() as u32)
1401        .unwrap_or(0);
1402
1403    if state.c_calls() >= LUAI_MAXCCALLS {
1404        return resume_error(state, b"C stack overflow", nargs);
1405    }
1406    state.nCcalls += 1;
1407
1408    // C: luai_userstateresume(L, nargs) — no-op (macros.tsv)
1409    // C: api_checknelems(L, ...)
1410    debug_assert!(
1411        if state.status == LuaStatus::Ok as u8 {
1412            nargs + 1 <= state.top_idx().0 as i32
1413        } else {
1414            nargs <= state.top_idx().0 as i32
1415        },
1416        "lua_resume: not enough stack elements"
1417    );
1418
1419    // C: status = luaD_rawrunprotected(L, resume, &nargs)
1420    // PORT NOTE: In C, luaD_throw pushes the error value onto the stack before
1421    // longjmp-ing. In Rust the value rides inside LuaError and is normally
1422    // discarded by raw_run_protected — but real errors (ErrRun/ErrMem/etc.)
1423    // need their payload pushed so the later seterrorobj can copy it back to
1424    // the error slot. We must skip Yield (no payload) and Ok (none happened).
1425    let (mut status, err_value) = match raw_run_protected(state, |s| resume_coroutine(s, nargs)) {
1426        Ok(()) => (LuaStatus::Ok, None),
1427        Err(e) => {
1428            let s = e.to_status();
1429            let v = if error_status(s) { Some(e.into_value()) } else { None };
1430            (s, v)
1431        }
1432    };
1433    if let Some(v) = err_value {
1434        state.push(v);
1435    }
1436
1437    // C: status = precover(L, status)
1438    status = precover(state, status);
1439
1440    if !error_status(status) {
1441        // C: lua_assert(status == L->status)
1442        debug_assert!(status as u8 == state.status, "lua_resume: status mismatch");
1443    } else {
1444        // Unrecoverable error — mark thread as dead
1445        state.status = status as u8;
1446        // C: luaD_seterrorobj(L, status, L->top.p)
1447        let top = state.top_idx();
1448        set_error_obj(state, status, top);
1449        // C: L->ci->top.p = L->top.p
1450        let new_top = state.top_idx();
1451        let ci_idx = state.ci;
1452        state.get_ci_mut(ci_idx).top = new_top;
1453    }
1454
1455    // C: *nresults = (status == LUA_YIELD) ? L->ci->u2.nyield : cast_int(L->top.p - (L->ci->func.p + 1))
1456    let ci_idx = state.ci;
1457    *nresults = if status == LuaStatus::Yield {
1458        state.get_ci_u2_nyield(ci_idx)
1459    } else {
1460        let ci_func = state.get_ci(ci_idx).func;
1461        state.top_idx().0 as i32 - (ci_func.0 as i32 + 1)
1462    };
1463
1464    // C: lua_unlock(L) — no-op
1465    status
1466}
1467
1468/// Returns whether the calling context can yield.
1469///
1470/// C: `LUA_API int lua_isyieldable(lua_State *L)`
1471pub fn lua_isyieldable(state: &LuaState) -> bool {
1472    // C: return yieldable(L)
1473    // yieldable → state.is_yieldable()  (macros.tsv)
1474    state.is_yieldable()
1475}
1476
1477/// Yields the current coroutine, saving the continuation function `k` and
1478/// context `ctx` for resumption.
1479///
1480/// C: `LUA_API int lua_yieldk(lua_State *L, int nresults, lua_KContext ctx, lua_KFunction k)`
1481pub fn lua_yieldk(
1482    state: &mut LuaState,
1483    nresults: i32,
1484    ctx: isize,
1485    k: Option<crate::state::LuaKFunction>,
1486) -> Result<i32, LuaError> {
1487    // TODO(port): coroutine support (Phase E). Yielding requires stack-switching;
1488    // stubbed here with a faithful translation of the C logic.
1489
1490    // C: luai_userstateyield(L, nresults) — no-op (macros.tsv)
1491    // C: lua_lock(L) — no-op
1492    let ci_idx = state.ci;
1493
1494    // C: api_checknelems(L, nresults)
1495    debug_assert!(
1496        nresults <= state.top_idx().0 as i32,
1497        "lua_yieldk: not enough elements on stack"
1498    );
1499
1500    // C: if (l_unlikely(!yieldable(L)))
1501    if !state.is_yieldable() {
1502        if !state.is_main_thread() {
1503            // C: luaG_runerror(L, "attempt to yield across a C-call boundary")
1504            return Err(LuaError::runtime(format_args!(
1505                "attempt to yield across a C-call boundary"
1506            )));
1507        } else {
1508            // C: luaG_runerror(L, "attempt to yield from outside a coroutine")
1509            return Err(LuaError::runtime(format_args!(
1510                "attempt to yield from outside a coroutine"
1511            )));
1512        }
1513    }
1514
1515    // C: L->status = LUA_YIELD
1516    state.status = LuaStatus::Yield as u8;
1517    // C: ci->u2.nyield = nresults
1518    state.set_ci_u2_nyield(ci_idx, nresults);
1519
1520    if state.get_ci(ci_idx).is_lua() {
1521        // C: inside a hook
1522        debug_assert!(!state.get_ci(ci_idx).is_lua_code());
1523        debug_assert!(nresults == 0, "hooks cannot yield values");
1524        debug_assert!(k.is_none(), "hooks cannot continue after yielding");
1525        // Fall through — hook yields return 0 to luaD_hook.
1526    } else {
1527        // C: if ((ci->u.c.k = k) != NULL) ci->u.c.ctx = ctx;
1528        // TODO(phase-b): mutate u_c.k/u_c.ctx fields directly inside CallInfoFrame::C.
1529        if let crate::state::CallInfoFrame::C { k: ref mut frame_k, ctx: ref mut frame_ctx, .. } =
1530            state.get_ci_mut(ci_idx).u {
1531            *frame_k = k;
1532            if k.is_some() {
1533                *frame_ctx = ctx;
1534            }
1535        }
1536        // C: luaD_throw(L, LUA_YIELD)
1537        // In Rust: return Err to propagate the yield signal up the call stack.
1538        return Err(LuaError::Yield);
1539    }
1540
1541    // C: lua_assert(ci->callstatus & CIST_HOOKED)  — must be inside a hook
1542    debug_assert!(
1543        state.get_ci(ci_idx).callstatus & CIST_HOOKED != 0,
1544        "lua_yieldk called outside a hook"
1545    );
1546    // C: lua_unlock(L) — no-op
1547    Ok(0) // return to luaD_hook
1548}
1549
1550// ══════════════════════════════════════════════════════════════════════════════
1551// Protected close
1552// ══════════════════════════════════════════════════════════════════════════════
1553
1554/// Auxiliary data for `close_aux`.
1555///
1556/// C: `struct CloseP { StkId level; int status; }`
1557struct CloseP {
1558    level: StackIdx,
1559    status: LuaStatus,
1560}
1561
1562/// Calls `luaF_close` with the level/status captured in `pcl`.
1563///
1564/// C: `static void closepaux(lua_State *L, void *ud)`
1565fn close_aux(state: &mut LuaState, pcl: &mut CloseP) -> Result<(), LuaError> {
1566    // C: luaF_close(L, pcl->level, pcl->status, 0)
1567    // TODO(port): status→i32 conversion for func::close sentinel.
1568    func::close(state, pcl.level, pcl.status as i32, false)?;
1569    Ok(())
1570}
1571
1572/// Calls `luaF_close` in protected mode, retrying on error.
1573/// Returns the original `status` on clean completion, or the new error status.
1574///
1575/// C: `int luaD_closeprotected(lua_State *L, ptrdiff_t level, int status)`
1576pub(crate) fn close_protected(
1577    state: &mut LuaState,
1578    level: StackIdx,
1579    status: LuaStatus,
1580) -> LuaStatus {
1581    let old_ci = state.ci;
1582    let old_allowhook = state.allowhook;
1583    let mut status = status;
1584
1585    loop {
1586        // C: pcl.level = restorestack(L, level) — StackIdx already stable
1587        let mut pcl = CloseP { level, status };
1588        // C: status = luaD_rawrunprotected(L, &closepaux, &pcl)
1589        let (run_status, err_value) = match raw_run_protected(state, |s| close_aux(s, &mut pcl)) {
1590            Ok(()) => (LuaStatus::Ok, None),
1591            Err(e) => (e.to_status(), Some(e.into_value())),
1592        };
1593        if run_status == LuaStatus::Ok {
1594            // C: return pcl.status
1595            return pcl.status;
1596        }
1597        // C: L->ci = old_ci; L->allowhook = old_allowhooks;
1598        state.ci = old_ci;
1599        state.allowhook = old_allowhook;
1600        // In C, luaD_throw pushed the error value onto the stack at top before
1601        // long-jumping, which leaves it at `top - 1` for the next iteration's
1602        // luaD_seterrorobj to copy. In Rust the value rides inside the
1603        // LuaError; push it explicitly so the next iteration (and the outer
1604        // pcall's seterrorobj) can read it at `top - 1`.
1605        if let Some(v) = err_value {
1606            state.push(v);
1607        }
1608        status = run_status;
1609    }
1610}
1611
1612/// Calls function `func` in protected mode, restoring thread state on error.
1613/// Returns `LuaStatus::Ok` on success, or an error status.
1614///
1615/// C: `int luaD_pcall(lua_State *L, Pfunc func, void *u, ptrdiff_t old_top, ptrdiff_t ef)`
1616pub(crate) fn pcall<F>(
1617    state: &mut LuaState,
1618    func: F,
1619    old_top: StackIdx,
1620    ef: isize,
1621) -> LuaStatus
1622where
1623    F: FnOnce(&mut LuaState) -> Result<(), LuaError>,
1624{
1625    let old_ci = state.ci;
1626    let old_allowhook = state.allowhook;
1627    let old_errfunc = state.errfunc;
1628    // C: L->errfunc = ef
1629    state.errfunc = ef;
1630
1631    // C: status = luaD_rawrunprotected(L, func, u)
1632    // PORT NOTE: In C, luaD_throw pushes the error value onto the stack before
1633    // longjmp-ing, and luaG_errormsg invokes the message handler at the error
1634    // site before the throw. In Rust the error rides inside LuaError and
1635    // propagates via `?`, so the handler is never invoked along the way; we
1636    // synthesise that invocation here once we've caught the Err.
1637    let mut status = match raw_run_protected(state, func) {
1638        Ok(()) => LuaStatus::Ok,
1639        Err(e) => {
1640            let s = e.to_status();
1641            state.push(e.into_value());
1642            if ef != 0 && error_status(s) && s != LuaStatus::ErrErr {
1643                let errfunc_idx = StackIdx(ef as u32);
1644                let arg = state.get_at(state.top_idx() - 1).clone();
1645                state.push(arg);
1646                let handler = state.get_at(errfunc_idx).clone();
1647                state.set_at(state.top_idx() - 2, handler);
1648                match state.call_no_yield(state.top_idx() - 2, 1) {
1649                    Ok(()) => s,
1650                    Err(_) => LuaStatus::ErrErr,
1651                }
1652            } else {
1653                s
1654            }
1655        }
1656    };
1657
1658    if status != LuaStatus::Ok {
1659        // C: L->ci = old_ci; L->allowhook = old_allowhooks;
1660        state.ci = old_ci;
1661        state.allowhook = old_allowhook;
1662        // C: status = luaD_closeprotected(L, old_top, status)
1663        status = close_protected(state, old_top, status);
1664        // C: luaD_seterrorobj(L, status, restorestack(L, old_top))
1665        // restorestack → old_top  (already a StackIdx)
1666        set_error_obj(state, status, old_top);
1667        // C: luaD_shrinkstack(L)
1668        shrink_stack(state);
1669    }
1670
1671    // C: L->errfunc = old_errfunc
1672    state.errfunc = old_errfunc;
1673    status
1674}
1675
1676// ══════════════════════════════════════════════════════════════════════════════
1677// Protected parser
1678// ══════════════════════════════════════════════════════════════════════════════
1679
1680/// Parser invocation data passed through `pcall`.
1681///
1682/// C: `struct SParser { ZIO *z; Mbuffer buff; Dyndata dyd; const char *mode; const char *name; }`
1683///
1684/// PORT NOTE: `const char *mode` and `const char *name` become owned byte vecs
1685/// so that `SParser` can outlive the original string data without raw pointers.
1686struct SParser {
1687    z: ZIO,
1688    /// LexBuffer from `crate::zio` (Mbuffer in C).
1689    buff: LexBuffer,
1690    /// TODO(phase-b): real Dyndata lives in the lua-parse crate.
1691    dyd: DynDataStub,
1692    // C: const char *mode — byte slice for chunk mode ("b", "t", or "bt")
1693    // PORT NOTE: stored as Option<Vec<u8>> to own the bytes; None means no mode restriction.
1694    mode: Option<Vec<u8>>,
1695    // C: const char *name — chunk name (source identifier)
1696    name: Vec<u8>,
1697}
1698
1699/// Checks that the chunk mode permits loading the given kind ("binary" or "text").
1700///
1701/// C: `static void checkmode(lua_State *L, const char *mode, const char *x)`
1702fn check_mode(
1703    state: &mut LuaState,
1704    mode: Option<&[u8]>,
1705    kind: &[u8],
1706) -> Result<(), LuaError> {
1707    if let Some(mode_bytes) = mode {
1708        // C: strchr(mode, x[0]) == NULL  — mode doesn't contain the first letter of kind
1709        let kind_char = kind[0];
1710        if !mode_bytes.contains(&kind_char) {
1711            // C: luaO_pushfstring + luaD_throw(L, LUA_ERRSYNTAX)
1712            // TODO(port): &[u8] display — lossy UTF-8 here is acceptable for mode/kind
1713            // strings which are always ASCII literals ("binary"/"text" and "bt"/"b"/"t").
1714            return Err(LuaError::syntax(format_args!(
1715                "attempt to load a {} chunk (mode is '{}')",
1716                core::str::from_utf8(kind).unwrap_or("?"),
1717                core::str::from_utf8(mode_bytes).unwrap_or("?"),
1718            )));
1719        }
1720    }
1721    Ok(())
1722}
1723
1724/// Parser callback invoked inside `pcall`: reads the first byte to decide
1725/// binary vs. text, then calls the undumper or parser accordingly.
1726///
1727/// C: `static void f_parser(lua_State *L, void *ud)`
1728fn f_parser(state: &mut LuaState, p: &mut SParser) -> Result<(), LuaError> {
1729    // C: int c = zgetc(p->z)  — read first character
1730    // zgetc → z.getc()  (macros.tsv)
1731    let c = p.z.getc();
1732
1733    // C: if (c == LUA_SIGNATURE[0])
1734    // LUA_SIGNATURE → const LUA_SIGNATURE: &[u8] = b"\x1bLua"  (macros.tsv)
1735    let cl = if c == b'\x1b' as i32 {
1736        // C: checkmode(L, p->mode, "binary")
1737        check_mode(state, p.mode.as_deref(), b"binary")?;
1738        // C: cl = luaU_undump(L, p->z, p->name)
1739        // TODO(port): undump returns a LClosure; the Rust API isn't finalised.
1740        crate::undump::undump(state, &mut p.z, &p.name)?
1741    } else {
1742        // C: checkmode(L, p->mode, "text")
1743        check_mode(state, p.mode.as_deref(), b"text")?;
1744        // C: cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c)
1745        // TODO(port): parser API not yet finalised; returns a LClosure.
1746        parse_stub(state, &mut p.z, &mut p.buff, &mut p.dyd, &p.name, c)?
1747    };
1748
1749    // C: lua_assert(cl->nupvalues == cl->p->sizeupvalues)
1750    debug_assert!(cl.upvals.len() == cl.proto.upvalues.len());
1751    // C: luaF_initupvals(L, cl)
1752    func::init_upvals(state, &cl)?;
1753
1754    // PORT NOTE: In C-Lua, `luaY_parser` / `luaU_undump` themselves push the
1755    // closure onto the stack before returning (see lparser.c `luaY_parser`:
1756    // `setclLvalue2s(L, L->top.p, cl); luaD_inctop(L);`). In the Rust port
1757    // they return the closure by value, so `f_parser` must push it here.
1758    // Without this, the caller (`api::load`) sees stale Nil at top-1 and any
1759    // subsequent `pcall_k(state, 0, ...)` fails with "attempt to call a nil
1760    // value".
1761    state.check_stack(1)?;
1762    state.push(LuaValue::Function(LuaClosure::Lua(cl)));
1763
1764    Ok(())
1765}
1766
1767/// Loads and parses a chunk in protected mode, returning the status.
1768///
1769/// C: `int luaD_protectedparser(lua_State *L, ZIO *z, const char *name, const char *mode)`
1770pub(crate) fn protected_parser(
1771    state: &mut LuaState,
1772    z: ZIO,
1773    name: &[u8],
1774    mode: Option<&[u8]>,
1775) -> LuaStatus {
1776    // C: incnny(L)  — cannot yield during parsing
1777    // incnny → state.inc_nny()  (macros.tsv)
1778    state.inc_nny();
1779
1780    let mut p = SParser {
1781        z,
1782        buff: LexBuffer::new(),
1783        dyd: DynDataStub::new(),
1784        mode: mode.map(|m| m.to_vec()),
1785        name: name.to_vec(),
1786    };
1787
1788    // C: luaZ_initbuffer(L, &p.buff) — LexBuffer::new() already initialised above
1789    // (macros.tsv: luaZ_initbuffer → buf.init() / Mbuffer::new())
1790
1791    // C: status = luaD_pcall(L, f_parser, &p, savestack(L, L->top.p), L->errfunc)
1792    let top_idx = state.top_idx();
1793    let errfunc = state.errfunc;
1794    let status = pcall(state, |s| f_parser(s, &mut p), top_idx, errfunc);
1795
1796    // C: luaZ_freebuffer(L, &p.buff) — Rust's Drop handles deallocation (macros.tsv)
1797    // C: luaM_freearray(L, p.dyd.actvar.arr, ...) — Rust's Drop handles Vec (macros.tsv)
1798    // (p and all its sub-fields drop here automatically)
1799
1800    // C: decnny(L)
1801    // decnny → state.dec_nny()  (macros.tsv)
1802    state.dec_nny();
1803
1804    status
1805}
1806
1807// ──────────────────────────────────────────────────────────────────────────
1808// PORT STATUS
1809//   source:        src/ldo.c  (1029 lines, ~37 functions translated, 2 omitted)
1810//   target_crate:  lua-vm
1811//   confidence:    medium
1812//   todos:         23
1813//   port_notes:    13
1814//   unsafe_blocks: 0
1815//   notes:         Core call/stack/error machinery translated faithfully.
1816//                  setjmp/longjmp → Result<T,LuaError> throughout.
1817//                  relstack/correctstack omitted (StackIdx already offset-based).
1818//                  Coroutine functions (lua_resume, lua_yieldk, resume, unroll,
1819//                  etc.) are translated but require Phase E stack-switching to
1820//                  actually work.  Hook-callback borrow conflict flagged as
1821//                  TODO(port) in hook() and finish_ccall(); Phase E must solve.
1822//                  All method calls (check_stack, gc_check_step, get_ci*,
1823//                  set_ci*, next_ci, etc.) are best-guess stubs to be wired
1824//                  up in Phase B once the LuaState API is finalised.
1825//                  PERF: `precall` split into a `#[inline(always)]` fast-path
1826//                  Lua-closure handler plus a `#[cold]` `precall_slow` for the
1827//                  C-closure / LightC / __call-metamethod arms.  Nil-fill of
1828//                  missing fixed params lives in a `#[cold] #[inline(never)]`
1829//                  helper so the no-fill case (overwhelmingly common — fib,
1830//                  any direct call with matching arity) is the predicted-taken
1831//                  branch.  fibonacci 2.65→2.38× (best-of-5) following this
1832//                  change, with proportional wins on closure_ops, table_ops,
1833//                  and table_ops_long.
1834// ──────────────────────────────────────────────────────────────────────────