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