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