Skip to main content

lua_vm/
do_.rs

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