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