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