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