Skip to main content

lua_vm/
do_.rs

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