Skip to main content

lua_vm/
vm.rs

1//! Lua virtual machine — port of `src/lvm.c` (1899 lines, 32 functions).
2//!
3//! This module implements:
4//! - Number coercion helpers (tonumber_, flttointeger, tointegerns, tointeger)
5//! - Numeric `for`-loop preparation and stepping (forlimit, forprep, floatforloop)
6//! - Table get/set with metamethod chaining (finishget, finishset)
7//! - String comparison respecting embedded NULs (l_strcmp)
8//! - Relational operators: lessthan, lessequal, equalobj (with metamethods)
9//! - String concatenation (concat)
10//! - Object length operator (objlen)
11//! - Integer arithmetic: idiv, mod, modf, shiftl
12//! - Closure creation (pushclosure)
13//! - Yield-resume bridge (finishOp)
14//! - Main interpreter loop (execute) — the Lua bytecode dispatch engine.
15//!
16//! # Control flow note
17//! The C source uses `goto startfunc` / `goto returning` / `goto ret` across
18//! labelled points in `luaV_execute`. These are modelled with Rust's labelled
19//! loops (`'startfunc`, `'returning`, `'dispatch`) and `continue`/`break`
20//! on those labels.  See inline `PORT NOTE` comments.
21
22
23#[allow(unused_imports)] use crate::prelude::*;
24use lua_types::{
25    CallInfoIdx, GcRef, LuaError, LuaValue, StackIdx,
26};
27use lua_types::tagmethod::TagMethod;
28use lua_types::opcode::Instruction;
29use crate::state::LuaState;
30
31/// TODO(phase-b): lua-types does not yet expose `OpCode`. Stubbed locally with
32/// all 5.4 opcodes so call sites in vm.rs/debug.rs resolve; the real numeric
33/// values and per-opcode mode flags live in `lua-types/src/opcode.rs` once
34/// translated.
35///
36/// `#[repr(u8)]` with explicit discriminants matching C-Lua's `lopcodes.h`
37/// numbering (0=OP_MOVE, 1=OP_LOADI, ..., 82=OP_EXTRAARG). The ordered, dense
38/// 0..=82 layout lets LLVM compile `opcode()` to a bounds-checked cast on the
39/// low 7 bits of the instruction word and fuse it with the dispatch `match`
40/// downstream. Discriminant order intentionally matches the integer keys in
41/// `InstructionExt::opcode`, not the prior compile-order grouping.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[allow(non_camel_case_types)]
44#[repr(u8)]
45pub enum OpCode {
46    Move = 0,
47    LoadI = 1,
48    LoadF = 2,
49    LoadK = 3,
50    LoadKX = 4,
51    LoadFalse = 5,
52    LFalseSkip = 6,
53    LoadTrue = 7,
54    LoadNil = 8,
55    GetUpVal = 9,
56    SetUpVal = 10,
57    GetTabUp = 11,
58    GetTable = 12,
59    GetI = 13,
60    GetField = 14,
61    SetTabUp = 15,
62    SetTable = 16,
63    SetI = 17,
64    SetField = 18,
65    NewTable = 19,
66    Self_ = 20,
67    AddI = 21,
68    AddK = 22,
69    SubK = 23,
70    MulK = 24,
71    ModK = 25,
72    PowK = 26,
73    DivK = 27,
74    IDivK = 28,
75    BAndK = 29,
76    BOrK = 30,
77    BXOrK = 31,
78    ShrI = 32,
79    ShlI = 33,
80    Add = 34,
81    Sub = 35,
82    Mul = 36,
83    Mod = 37,
84    Pow = 38,
85    Div = 39,
86    IDiv = 40,
87    BAnd = 41,
88    BOr = 42,
89    BXOr = 43,
90    Shl = 44,
91    Shr = 45,
92    MmBin = 46,
93    MmBinI = 47,
94    MmBinK = 48,
95    Unm = 49,
96    BNot = 50,
97    Not = 51,
98    Len = 52,
99    Concat = 53,
100    Close = 54,
101    Tbc = 55,
102    Jmp = 56,
103    Eq = 57,
104    Lt = 58,
105    Le = 59,
106    EqK = 60,
107    EqI = 61,
108    LtI = 62,
109    LeI = 63,
110    GtI = 64,
111    GeI = 65,
112    Test = 66,
113    TestSet = 67,
114    Call = 68,
115    TailCall = 69,
116    Return = 70,
117    Return0 = 71,
118    Return1 = 72,
119    ForLoop = 73,
120    ForPrep = 74,
121    TForPrep = 75,
122    TForCall = 76,
123    TForLoop = 77,
124    SetList = 78,
125    Closure = 79,
126    VarArg = 80,
127    VarArgPrep = 81,
128    ExtraArg = 82,
129}
130
131/// Number of distinct opcodes (matches C-Lua's `NUM_OPCODES`). Held for
132/// downstream debug/dump callers that count opcodes by name; the dispatch
133/// hot path in `InstructionExt::opcode` does its own per-arm match.
134#[allow(dead_code)]
135const NUM_OPCODES: u8 = 83;
136
137impl OpCode {
138    /// Legacy alias retained because the prior duplicate enum variant
139    /// `LoadKx` (case-typo of `LoadKX`) is still referenced from
140    /// `crates/lua-vm/src/debug.rs`. Both names denote the same C
141    /// `OP_LOADKX` opcode. Kept as an associated `const` so existing call
142    /// sites compile unchanged while the enum remains a clean 0..=82 dense
143    /// discriminant set required by `#[repr(u8)]`.
144    #[allow(non_upper_case_globals)]
145    pub const LoadKx: OpCode = OpCode::LoadKX;
146
147    /// Legacy alias for `GetUpVal` retained for the same reason as `LoadKx`.
148    #[allow(non_upper_case_globals)]
149    pub const GetUpval: OpCode = OpCode::GetUpVal;
150}
151
152/// TODO(phase-b): Instruction accessor extension trait. The real per-mode
153/// decode helpers live in `lua-types::opcode` once translated. Stubbed locally
154/// so call sites resolve; bodies are inferred from `lopcodes.h` macro shapes.
155pub trait InstructionExt {
156    fn opcode(&self) -> OpCode;
157    fn arg_a(&self) -> i32;
158    fn arg_b(&self) -> i32;
159    fn arg_c(&self) -> i32;
160    fn arg_k(&self) -> i32;
161    fn arg_ax(&self) -> i32;
162    fn arg_bx(&self) -> i32;
163    fn arg_s_b(&self) -> i32;
164    fn arg_s_c(&self) -> i32;
165    fn arg_s_j(&self) -> i32;
166    fn arg_s_bx(&self) -> i32;
167    fn test_k(&self) -> bool;
168    fn test_a_mode(&self) -> bool;
169    fn is_mm_mode(&self) -> bool;
170    fn is_vararg_prep(&self) -> bool;
171    fn is_in_top(&self) -> bool;
172}
173
174impl InstructionExt for Instruction {
175    ///
176    /// The 83-arm match looks expensive, but because `OpCode` is
177    /// `#[repr(u8)]` with explicit discriminants 0..=82 matching each match
178    /// arm's integer key exactly, LLVM compiles this to a single bounds
179    /// check + identity cast — no jump table, no memory indirection. The
180    /// previous array-lookup form forced an extra `OPCODE_TABLE` byte load
181    /// per dispatch tick that LLVM could not see through.
182    #[inline(always)]
183    fn opcode(&self) -> OpCode {
184        match (self.raw() & 0x7F) as u8 {
185            0 => OpCode::Move,
186            1 => OpCode::LoadI,
187            2 => OpCode::LoadF,
188            3 => OpCode::LoadK,
189            4 => OpCode::LoadKX,
190            5 => OpCode::LoadFalse,
191            6 => OpCode::LFalseSkip,
192            7 => OpCode::LoadTrue,
193            8 => OpCode::LoadNil,
194            9 => OpCode::GetUpVal,
195            10 => OpCode::SetUpVal,
196            11 => OpCode::GetTabUp,
197            12 => OpCode::GetTable,
198            13 => OpCode::GetI,
199            14 => OpCode::GetField,
200            15 => OpCode::SetTabUp,
201            16 => OpCode::SetTable,
202            17 => OpCode::SetI,
203            18 => OpCode::SetField,
204            19 => OpCode::NewTable,
205            20 => OpCode::Self_,
206            21 => OpCode::AddI,
207            22 => OpCode::AddK,
208            23 => OpCode::SubK,
209            24 => OpCode::MulK,
210            25 => OpCode::ModK,
211            26 => OpCode::PowK,
212            27 => OpCode::DivK,
213            28 => OpCode::IDivK,
214            29 => OpCode::BAndK,
215            30 => OpCode::BOrK,
216            31 => OpCode::BXOrK,
217            32 => OpCode::ShrI,
218            33 => OpCode::ShlI,
219            34 => OpCode::Add,
220            35 => OpCode::Sub,
221            36 => OpCode::Mul,
222            37 => OpCode::Mod,
223            38 => OpCode::Pow,
224            39 => OpCode::Div,
225            40 => OpCode::IDiv,
226            41 => OpCode::BAnd,
227            42 => OpCode::BOr,
228            43 => OpCode::BXOr,
229            44 => OpCode::Shl,
230            45 => OpCode::Shr,
231            46 => OpCode::MmBin,
232            47 => OpCode::MmBinI,
233            48 => OpCode::MmBinK,
234            49 => OpCode::Unm,
235            50 => OpCode::BNot,
236            51 => OpCode::Not,
237            52 => OpCode::Len,
238            53 => OpCode::Concat,
239            54 => OpCode::Close,
240            55 => OpCode::Tbc,
241            56 => OpCode::Jmp,
242            57 => OpCode::Eq,
243            58 => OpCode::Lt,
244            59 => OpCode::Le,
245            60 => OpCode::EqK,
246            61 => OpCode::EqI,
247            62 => OpCode::LtI,
248            63 => OpCode::LeI,
249            64 => OpCode::GtI,
250            65 => OpCode::GeI,
251            66 => OpCode::Test,
252            67 => OpCode::TestSet,
253            68 => OpCode::Call,
254            69 => OpCode::TailCall,
255            70 => OpCode::Return,
256            71 => OpCode::Return0,
257            72 => OpCode::Return1,
258            73 => OpCode::ForLoop,
259            74 => OpCode::ForPrep,
260            75 => OpCode::TForPrep,
261            76 => OpCode::TForCall,
262            77 => OpCode::TForLoop,
263            78 => OpCode::SetList,
264            79 => OpCode::Closure,
265            80 => OpCode::VarArg,
266            81 => OpCode::VarArgPrep,
267            82 => OpCode::ExtraArg,
268            _ => OpCode::ExtraArg,
269        }
270    }
271    #[inline] fn arg_a(&self) -> i32 { ((self.raw() >> 7) & 0xFF) as i32 }
272    #[inline] fn arg_b(&self) -> i32 { ((self.raw() >> 16) & 0xFF) as i32 }
273    #[inline] fn arg_c(&self) -> i32 { ((self.raw() >> 24) & 0xFF) as i32 }
274    #[inline] fn arg_k(&self) -> i32 { ((self.raw() >> 15) & 0x1) as i32 }
275    #[inline] fn arg_ax(&self) -> i32 { (self.raw() >> 7) as i32 }
276    #[inline] fn arg_bx(&self) -> i32 { (self.raw() >> 15) as i32 }
277    #[inline] fn arg_s_b(&self) -> i32 { self.arg_b() - 0x7F }
278    #[inline] fn arg_s_c(&self) -> i32 { self.arg_c() - 0x7F }
279    #[inline] fn arg_s_j(&self) -> i32 { self.arg_ax() - 0xFFFFFF }
280    #[inline] fn arg_s_bx(&self) -> i32 { self.arg_bx() - 0xFFFF }
281    #[inline] fn test_k(&self) -> bool { (self.raw() & (1 << 15)) != 0 }
282    #[inline]
283    fn test_a_mode(&self) -> bool {
284        (op_mode_byte(self.opcode()) & (1 << 3)) != 0
285    }
286    #[inline]
287    fn is_mm_mode(&self) -> bool {
288        (op_mode_byte(self.opcode()) & (1 << 7)) != 0
289    }
290    #[inline]
291    fn is_vararg_prep(&self) -> bool {
292        matches!(self.opcode(), OpCode::VarArgPrep)
293    }
294    #[inline]
295    fn is_in_top(&self) -> bool {
296        (op_mode_byte(self.opcode()) & (1 << 5)) != 0 && self.arg_b() == 0
297    }
298}
299
300///
301/// Layout (from lopcodes.h `opmode` macro):
302///   bit 7: MM (metamethod call)
303///   bit 6: OT (instruction sets `L->top` for next when C == 0)
304///   bit 5: IT (instruction reads `L->top` from prev when B == 0)
305///   bit 4: T  (test; next instruction must be a jump)
306///   bit 3: A  (instruction writes register A)
307///   bits 0-2: op format mode (iABC, iABx, iAsBx, iAx, isJ)
308///
309/// PORT NOTE: lua-types does not yet expose the canonical `OP_MODES` table; this
310/// is a local stand-in keyed off the vm.rs `OpCode` stub so the four mode
311/// predicates above can answer correctly until the real table lands.
312const OP_MODE_BYTES: [u8; NUM_OPCODES as usize] = [
313    0x08, // Move
314    0x0a, // LoadI
315    0x0a, // LoadF
316    0x09, // LoadK
317    0x09, // LoadKX
318    0x08, // LoadFalse
319    0x08, // LFalseSkip
320    0x08, // LoadTrue
321    0x08, // LoadNil
322    0x08, // GetUpVal
323    0x00, // SetUpVal
324    0x08, // GetTabUp
325    0x08, // GetTable
326    0x08, // GetI
327    0x08, // GetField
328    0x00, // SetTabUp
329    0x00, // SetTable
330    0x00, // SetI
331    0x00, // SetField
332    0x08, // NewTable
333    0x08, // Self_
334    0x08, // AddI
335    0x08, // AddK
336    0x08, // SubK
337    0x08, // MulK
338    0x08, // ModK
339    0x08, // PowK
340    0x08, // DivK
341    0x08, // IDivK
342    0x08, // BAndK
343    0x08, // BOrK
344    0x08, // BXOrK
345    0x08, // ShrI
346    0x08, // ShlI
347    0x08, // Add
348    0x08, // Sub
349    0x08, // Mul
350    0x08, // Mod
351    0x08, // Pow
352    0x08, // Div
353    0x08, // IDiv
354    0x08, // BAnd
355    0x08, // BOr
356    0x08, // BXOr
357    0x08, // Shl
358    0x08, // Shr
359    0x80, // MmBin
360    0x80, // MmBinI
361    0x80, // MmBinK
362    0x08, // Unm
363    0x08, // BNot
364    0x08, // Not
365    0x08, // Len
366    0x08, // Concat
367    0x00, // Close
368    0x00, // Tbc
369    0x04, // Jmp
370    0x10, // Eq
371    0x10, // Lt
372    0x10, // Le
373    0x10, // EqK
374    0x10, // EqI
375    0x10, // LtI
376    0x10, // LeI
377    0x10, // GtI
378    0x10, // GeI
379    0x10, // Test
380    0x18, // TestSet
381    0x68, // Call
382    0x68, // TailCall
383    0x20, // Return
384    0x00, // Return0
385    0x00, // Return1
386    0x09, // ForLoop
387    0x09, // ForPrep
388    0x01, // TForPrep
389    0x00, // TForCall
390    0x09, // TForLoop
391    0x20, // SetList
392    0x09, // Closure
393    0x48, // VarArg
394    0x28, // VarArgPrep
395    0x03, // ExtraArg
396];
397
398#[inline(always)]
399fn op_mode_byte(op: OpCode) -> u8 {
400    OP_MODE_BYTES[op as usize]
401}
402
403// ─── Constants ───────────────────────────────────────────────────────────────
404
405/// Limit for tag-method chains to avoid infinite loops.
406const MAX_TAG_LOOP: i32 = 2000;
407
408const NBITS: u32 = 64;
409
410// ─── F2Imod — float-to-integer rounding mode ────────────────────────────────
411
412/// Rounding mode for float→integer coercions.
413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
414pub(crate) enum F2Imod {
415    /// Accept only exact integral values (no rounding).
416    Eq,
417    /// Round toward negative infinity.
418    Floor,
419    /// Round toward positive infinity.
420    Ceil,
421}
422
423// ─── Integer-overflow-safe helpers ──────────────────────────────────────────
424
425#[inline]
426fn intop_add(a: i64, b: i64) -> i64 {
427    (a as u64).wrapping_add(b as u64) as i64
428}
429
430#[inline]
431fn intop_sub(a: i64, b: i64) -> i64 {
432    (a as u64).wrapping_sub(b as u64) as i64
433}
434
435#[inline]
436fn intop_mul(a: i64, b: i64) -> i64 {
437    (a as u64).wrapping_mul(b as u64) as i64
438}
439
440/// Shifts via unsigned intermediate to get logical (not arithmetic) semantics.
441#[inline]
442fn intop_shr(x: i64, n: u32) -> i64 {
443    // PERF(port): logical right shift via unsigned; matches C unsigned semantics
444    (x as u64 >> n) as i64
445}
446
447#[inline]
448fn intop_shl(x: i64, n: u32) -> i64 {
449    (x as u64).wrapping_shl(n) as i64
450}
451
452#[inline]
453fn intop_band(a: i64, b: i64) -> i64 { ((a as u64) & (b as u64)) as i64 }
454#[inline]
455fn intop_bor(a: i64, b: i64) -> i64  { ((a as u64) | (b as u64)) as i64 }
456#[inline]
457fn intop_bxor(a: i64, b: i64) -> i64 { ((a as u64) ^ (b as u64)) as i64 }
458
459// ─── l_intfitsf ─────────────────────────────────────────────────────────────
460
461/// f64 has 53 bits of mantissa (including implicit leading 1).
462/// All i64 values with |i| <= 2^53 are exactly representable.
463#[inline]
464fn int_fits_float(i: i64) -> bool {
465    const MAXINTFITSF: u64 = 1u64 << f64::MANTISSA_DIGITS;
466    (MAXINTFITSF.wrapping_add(i as u64)) <= 2 * MAXINTFITSF
467}
468
469// ─── Private helper: string-to-number coercion ──────────────────────────────
470
471/// Attempt to convert a string value to a number in-place.
472/// Returns `Some(LuaValue)` with the numeric result, or `None` if the
473/// value is not a string or cannot be parsed as a numeral.
474fn str_to_number(obj: &LuaValue) -> Option<LuaValue> {
475    // cvt2num(o) = matches!(o, LuaValue::Str(_))
476    let s = match obj {
477        LuaValue::Str(ts) => ts.as_bytes().to_vec(),
478        _ => return None,
479    };
480    // Trim whitespace as Lua allows spaces around numerals in coercions.
481    let trimmed = trim_whitespace(&s);
482    if trimmed.is_empty() {
483        return None;
484    }
485    let mut result = LuaValue::Nil;
486    if crate::object::str2num(trimmed, &mut result) != 0 {
487        return Some(result);
488    }
489    None
490}
491
492fn trim_whitespace(s: &[u8]) -> &[u8] {
493    let start = s.iter().position(|&b| !b.is_ascii_whitespace()).unwrap_or(s.len());
494    let end = s.iter().rposition(|&b| !b.is_ascii_whitespace()).map(|i| i + 1).unwrap_or(0);
495    if start <= end { &s[start..end] } else { &s[0..0] }
496}
497
498// ─── Number coercion (public API matching lvm.h exports) ────────────────────
499
500/// Convert `obj` to f64, with string coercion.  Returns `Some(f64)` on
501/// success.  The fast path (already float) is handled by the caller's
502/// `tonumber` macro (inlined at call sites).
503pub(crate) fn tonumber_(obj: &LuaValue) -> Option<f64> {
504    if let LuaValue::Int(i) = obj {
505        return Some(*i as f64);
506    }
507    if let Some(v) = str_to_number(obj) {
508        return match v {
509            LuaValue::Float(f) => Some(f),
510            LuaValue::Int(i) => Some(i as f64),
511            _ => None,
512        };
513    }
514    None
515}
516
517/// Full numeric coercion including the float fast-path that `tonumber_` omits.
518fn tonumber(obj: &LuaValue) -> Option<f64> {
519    if let LuaValue::Float(f) = obj {
520        return Some(*f);
521    }
522    tonumber_(obj)
523}
524
525/// Convert float `n` to an integer according to `mode`.
526/// Returns `Some(i64)` on success.
527pub(crate) fn flt_to_integer(n: f64, mode: F2Imod) -> Option<i64> {
528    let f = n.floor();
529    if n != f {
530        match mode {
531            F2Imod::Eq => return None,
532            F2Imod::Ceil => {
533                // f = floor(n) + 1 = ceil(n) since n is not integral
534                let f = f + 1.0;
535                // lua_numbertointeger checks i64::MIN <= f <= i64::MAX
536                if f >= i64::MIN as f64 && f < (i64::MAX as f64 + 1.0) {
537                    return Some(f as i64);
538                }
539                return None;
540            }
541            F2Imod::Floor => { /* f is already floor(n) */ }
542        }
543    }
544    if f >= i64::MIN as f64 && f < (i64::MAX as f64 + 1.0) {
545        Some(f as i64)
546    } else {
547        None
548    }
549}
550
551/// Convert a value to integer without string coercion.
552pub(crate) fn to_integer_ns(obj: &LuaValue, mode: F2Imod) -> Option<i64> {
553    if let LuaValue::Float(f) = obj {
554        return flt_to_integer(*f, mode);
555    }
556    if let LuaValue::Int(i) = obj {
557        return Some(*i);
558    }
559    None
560}
561
562/// Convert a value to integer, with string coercion.
563pub(crate) fn to_integer(obj: &LuaValue, mode: F2Imod) -> Option<i64> {
564    let coerced;
565    let obj = if let Some(v) = str_to_number(obj) {
566        coerced = v;
567        &coerced
568    } else {
569        obj
570    };
571    to_integer_ns(obj, mode)
572}
573
574// ─── for-loop helpers ────────────────────────────────────────────────────────
575
576///                          lua_Integer *p, lua_Integer step)`
577/// Compute the integer loop limit.  Returns `Ok(true)` to skip the loop,
578/// `Ok(false)` with `*p` set to the limit, or `Err` if the limit is not a
579/// number at all.
580fn forlimit(
581    state: &mut LuaState,
582    init: i64,
583    lim: &LuaValue,
584    step: i64,
585) -> Result<(bool, i64), LuaError> {
586    let round = if step < 0 { F2Imod::Ceil } else { F2Imod::Floor };
587    if let Some(p) = to_integer(lim, round) {
588        let skip = if step > 0 { init > p } else { init < p };
589        return Ok((skip, p));
590    }
591    let flim = match tonumber(lim) {
592        Some(f) => f,
593        None => return Err(crate::debug::for_error(state, lim, b"limit")),
594    };
595    if 0.0_f64 < flim {
596        // positive → too large
597        if step < 0 {
598            return Ok((true, 0));
599        }
600        Ok((false, i64::MAX))
601    } else {
602        // negative → less than min integer
603        if step > 0 {
604            return Ok((true, 0));
605        }
606        Ok((false, i64::MIN))
607    }
608}
609
610/// Prepare a numeric `for` loop (OP_FORPREP).
611/// Stack layout at `ra`:
612///   ra+0: init, ra+1: limit, ra+2: step, ra+3: control variable (written here)
613/// Returns `Ok(true)` to skip the loop body entirely.
614pub(crate) fn forprep(state: &mut LuaState, ra: StackIdx) -> Result<bool, LuaError> {
615    let pinit  = state.get_at(ra);
616    let plimit = state.get_at(ra + 1);
617    let pstep  = state.get_at(ra + 2);
618
619    if let (LuaValue::Int(init), LuaValue::Int(step)) = (&pinit, &pstep) {
620        let init = *init;
621        let step = *step;
622        if step == 0 {
623            return Err(LuaError::runtime(format_args!("'for' step is zero")));
624        }
625        state.set_at(ra + 3, LuaValue::Int(init));
626
627        let (skip, limit) = forlimit(state, init, &plimit, step)?;
628        if skip {
629            return Ok(true);
630        }
631        let count: u64 = if step > 0 {
632            let c = (limit as u64).wrapping_sub(init as u64);
633            if step != 1 { c / (step as u64) } else { c }
634        } else {
635            let c = (init as u64).wrapping_sub(limit as u64);
636            c / (((-(step + 1)) as u64).wrapping_add(1))
637        };
638        state.set_at(ra + 1, LuaValue::Int(count as i64));
639        Ok(false)
640    } else {
641        let limit_f = match tonumber(&plimit) {
642            Some(f) => f,
643            None => return Err(crate::debug::for_error(state, &plimit, b"limit")),
644        };
645        let step_f = match tonumber(&pstep) {
646            Some(f) => f,
647            None => return Err(crate::debug::for_error(state, &pstep, b"step")),
648        };
649        let init_f = match tonumber(&pinit) {
650            Some(f) => f,
651            None => return Err(crate::debug::for_error(state, &pinit, b"initial value")),
652        };
653        if step_f == 0.0 {
654            return Err(LuaError::runtime(format_args!("'for' step is zero")));
655        }
656        let skip = if step_f > 0.0 { limit_f < init_f } else { init_f < limit_f };
657        if skip {
658            return Ok(true);
659        }
660        //    setfltvalue(s2v(ra), init); setfltvalue(s2v(ra+3), init);
661        state.set_at(ra + 1, LuaValue::Float(limit_f));
662        state.set_at(ra + 2, LuaValue::Float(step_f));
663        state.set_at(ra,     LuaValue::Float(init_f));
664        state.set_at(ra + 3, LuaValue::Float(init_f));
665        Ok(false)
666    }
667}
668
669/// Increments the float loop index and returns `true` if the loop continues.
670fn float_for_loop(state: &mut LuaState, ra: StackIdx) -> bool {
671    //    idx  = fltvalue(s2v(ra));
672    let step = match state.get_at(ra + 2) {
673        LuaValue::Float(f) => f,
674        _ => return false,
675    };
676    let limit = match state.get_at(ra + 1) {
677        LuaValue::Float(f) => f,
678        _ => return false,
679    };
680    let idx = match state.get_at(ra) {
681        LuaValue::Float(f) => f,
682        _ => return false,
683    };
684    let idx = idx + step;
685    if if step > 0.0 { idx <= limit } else { limit <= idx } {
686        state.set_at(ra,     LuaValue::Float(idx));
687        state.set_at(ra + 3, LuaValue::Float(idx));
688        true
689    } else {
690        false
691    }
692}
693
694// ─── Table get/set with metamethod chains ────────────────────────────────────
695
696///                          StkId val, const TValue *slot)`
697/// Finish a table-get with metamethod lookup.  `slot_was_none = true` means
698/// `t` is not a table and we should look for `__index` on `t` itself.
699pub(crate) fn finish_get(
700    state: &mut LuaState,
701    t_val: LuaValue,
702    key: LuaValue,
703    result_idx: StackIdx,
704    slot_empty: bool,
705    t_idx: Option<StackIdx>,
706) -> Result<(), LuaError> {
707    let mut t = t_val;
708    let mut t_idx = t_idx;
709    for _loop in 0..MAX_TAG_LOOP {
710        let tm: LuaValue;
711        if slot_empty && !matches!(t, LuaValue::Table(_)) {
712            tm = state.get_tm_by_obj(&t, TagMethod::Index);
713            if matches!(tm, LuaValue::Nil) {
714                return Err(match t_idx {
715                    Some(idx) => crate::debug::type_error(state, &t, idx, b"index"),
716                    None => LuaError::type_error(&t, "index"),
717                });
718            }
719        } else {
720            let mt = state.table_metatable(&t);
721            tm = state.fast_tm_table(mt.as_ref(), TagMethod::Index);
722            if matches!(tm, LuaValue::Nil) {
723                state.set_at(result_idx, LuaValue::Nil);
724                return Ok(());
725            }
726        }
727        if matches!(tm, LuaValue::Function(_)) {
728            state.call_tm_res(tm, &t, &key, result_idx)?;
729            return Ok(());
730        }
731        t = tm.clone();
732        t_idx = None;
733        if let Some(v) = state.fast_get(&t, &key)? {
734            state.set_at(result_idx, v);
735            return Ok(());
736        }
737        // else: loop — tail-call luaV_finishget
738    }
739    Err(LuaError::runtime(format_args!("'__index' chain too long; possible loop")))
740}
741
742///                          TValue *val, const TValue *slot)`
743/// Finish a table-set with `__newindex` metamethod lookup.
744///
745/// `var_hint` carries a `(kind, name)` pair (e.g. `(b"upvalue", b"a")`) used
746/// only when `t_idx` is None and the target is non-indexable — typically
747/// when the LHS is an upvalue (OP_SETTABUP). Pointer-identifying var_info
748/// won't recover the upvalue's name in that case, so the caller passes it
749/// in directly.
750pub(crate) fn finish_set(
751    state: &mut LuaState,
752    t_val: LuaValue,
753    key: LuaValue,
754    val: LuaValue,
755    _slot_present: bool,
756    t_idx: Option<StackIdx>,
757    var_hint: Option<(&[u8], &[u8])>,
758) -> Result<(), LuaError> {
759    let mut t = t_val;
760    let mut t_idx = t_idx;
761    for _loop in 0..MAX_TAG_LOOP {
762        let tm: LuaValue;
763        if matches!(t, LuaValue::Table(_)) {
764            let mt = state.table_metatable(&t);
765            tm = state.fast_tm_table(mt.as_ref(), TagMethod::NewIndex);
766            if matches!(tm, LuaValue::Nil) {
767                state.table_raw_set(&t, key, val.clone())?;
768                state.gc_barrier_back(&t, &val);
769                return Ok(());
770            }
771        } else {
772            tm = state.get_tm_by_obj(&t, TagMethod::NewIndex);
773            if matches!(tm, LuaValue::Nil) {
774                return Err(match (t_idx, var_hint) {
775                    (Some(idx), _) => crate::debug::type_error(state, &t, idx, b"index"),
776                    (None, Some((kind, name))) => {
777                        crate::debug::type_error_with_hint(state, &t, b"index", kind, name)
778                    }
779                    (None, None) => LuaError::type_error(&t, "index"),
780                });
781            }
782        }
783        if matches!(tm, LuaValue::Function(_)) {
784            state.call_tm(tm, &t, &key, &val)?;
785            return Ok(());
786        }
787        t = tm.clone();
788        t_idx = None;
789        if state.fast_get(&t, &key)?.is_some() {
790            state.table_raw_set(&t, key.clone(), val.clone())?;
791            state.gc_barrier_back(&t, &val);
792            return Ok(());
793        }
794    }
795    Err(LuaError::runtime(format_args!("'__newindex' chain too long; possible loop")))
796}
797
798// ─── String comparison ───────────────────────────────────────────────────────
799
800/// Lexicographic string comparison that handles embedded NULs by segmenting.
801/// Returns negative / zero / positive like `strcmp`.
802///
803/// PORT NOTE: C uses `strcoll` for locale-aware comparison within each NUL-free
804/// segment.  Rust's standard library has no locale support, so we use
805/// `slice::cmp` (byte-by-byte lexicographic order, equivalent to `memcmp`).
806/// This means locale-specific ordering (e.g. accented characters) differs from
807/// the C reference.  Mark as TODO for a later `libc::strcoll` bridge if needed.
808fn str_cmp(s1: &[u8], s2: &[u8]) -> std::cmp::Ordering {
809    // TODO(port): C uses strcoll per-segment; here we use byte-lexicographic
810    // order.  This affects locale-sensitive string comparisons.
811    let mut s1 = s1;
812    let mut s2 = s2;
813    loop {
814        // Find the first NUL in each slice to delimit a segment.
815        let z1 = s1.iter().position(|&b| b == 0).unwrap_or(s1.len());
816        let z2 = s2.iter().position(|&b| b == 0).unwrap_or(s2.len());
817        // Compare segment up to first NUL using byte order (not strcoll).
818        let seg_cmp = s1[..z1].cmp(&s2[..z2]);
819        if seg_cmp != std::cmp::Ordering::Equal {
820            return seg_cmp;
821        }
822        // Both segments compare equal up to the NUL position.
823        if z2 == s2.len() {
824            // s2 is finished
825            if z1 == s1.len() {
826                return std::cmp::Ordering::Equal;
827            }
828            return std::cmp::Ordering::Greater; // s1 has more
829        }
830        if z1 == s1.len() {
831            return std::cmp::Ordering::Less; // s1 finished, s2 has more
832        }
833        // Both have NULs; advance past them.
834        s1 = &s1[z1 + 1..];
835        s2 = &s2[z2 + 1..];
836    }
837}
838
839// ─── Comparison helpers (int vs float mixed comparisons) ────────────────────
840
841#[inline]
842fn lt_int_float(i: i64, f: f64) -> bool {
843    if int_fits_float(i) {
844        (i as f64) < f
845    } else {
846        match flt_to_integer(f, F2Imod::Ceil) {
847            Some(fi) => i < fi,
848            None => f > 0.0, // f is out of integer range; positive means i < f
849        }
850    }
851}
852
853#[inline]
854fn le_int_float(i: i64, f: f64) -> bool {
855    if int_fits_float(i) {
856        (i as f64) <= f
857    } else {
858        match flt_to_integer(f, F2Imod::Floor) {
859            Some(fi) => i <= fi,
860            None => f > 0.0,
861        }
862    }
863}
864
865#[inline]
866fn lt_float_int(f: f64, i: i64) -> bool {
867    if int_fits_float(i) {
868        f < (i as f64)
869    } else {
870        match flt_to_integer(f, F2Imod::Floor) {
871            Some(fi) => fi < i,
872            None => f < 0.0,
873        }
874    }
875}
876
877#[inline]
878fn le_float_int(f: f64, i: i64) -> bool {
879    if int_fits_float(i) {
880        f <= (i as f64)
881    } else {
882        match flt_to_integer(f, F2Imod::Ceil) {
883            Some(fi) => fi <= i,
884            None => f < 0.0,
885        }
886    }
887}
888
889#[inline]
890fn lt_num(l: &LuaValue, r: &LuaValue) -> bool {
891    debug_assert!(matches!(l, LuaValue::Int(_) | LuaValue::Float(_)));
892    debug_assert!(matches!(r, LuaValue::Int(_) | LuaValue::Float(_)));
893    match (l, r) {
894        (LuaValue::Int(li), LuaValue::Int(ri))     => li < ri,
895        (LuaValue::Int(li), LuaValue::Float(rf))   => lt_int_float(*li, *rf),
896        (LuaValue::Float(lf), LuaValue::Float(rf)) => lf < rf,
897        (LuaValue::Float(lf), LuaValue::Int(ri))   => lt_float_int(*lf, *ri),
898        _ => false,
899    }
900}
901
902#[inline]
903fn le_num(l: &LuaValue, r: &LuaValue) -> bool {
904    debug_assert!(matches!(l, LuaValue::Int(_) | LuaValue::Float(_)));
905    debug_assert!(matches!(r, LuaValue::Int(_) | LuaValue::Float(_)));
906    match (l, r) {
907        (LuaValue::Int(li), LuaValue::Int(ri))     => li <= ri,
908        (LuaValue::Int(li), LuaValue::Float(rf))   => le_int_float(*li, *rf),
909        (LuaValue::Float(lf), LuaValue::Float(rf)) => lf <= rf,
910        (LuaValue::Float(lf), LuaValue::Int(ri))   => le_float_int(*lf, *ri),
911        _ => false,
912    }
913}
914
915/// `l < r` for non-numbers (strings or metamethod).
916fn less_than_others(state: &mut LuaState, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
917    debug_assert!(!(matches!(l, LuaValue::Int(_) | LuaValue::Float(_))
918                  && matches!(r, LuaValue::Int(_) | LuaValue::Float(_))));
919    match (l, r) {
920        (LuaValue::Str(ts1), LuaValue::Str(ts2)) => {
921            Ok(str_cmp(ts1.as_bytes(), ts2.as_bytes()) == std::cmp::Ordering::Less)
922        }
923        _ => state.call_order_tm(l, r, TagMethod::Lt),
924    }
925}
926
927pub(crate) fn less_than(state: &mut LuaState, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
928    if matches!(l, LuaValue::Int(_) | LuaValue::Float(_))
929        && matches!(r, LuaValue::Int(_) | LuaValue::Float(_))
930    {
931        Ok(lt_num(l, r))
932    } else {
933        less_than_others(state, l, r)
934    }
935}
936
937fn less_equal_others(state: &mut LuaState, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
938    match (l, r) {
939        (LuaValue::Str(ts1), LuaValue::Str(ts2)) => {
940            Ok(str_cmp(ts1.as_bytes(), ts2.as_bytes()) != std::cmp::Ordering::Greater)
941        }
942        _ => state.call_order_tm(l, r, TagMethod::Le),
943    }
944}
945
946pub(crate) fn less_equal(state: &mut LuaState, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
947    if matches!(l, LuaValue::Int(_) | LuaValue::Float(_))
948        && matches!(r, LuaValue::Int(_) | LuaValue::Float(_))
949    {
950        Ok(le_num(l, r))
951    } else {
952        less_equal_others(state, l, r)
953    }
954}
955
956// ─── Equality ────────────────────────────────────────────────────────────────
957
958/// Main equality test.  `raw = true` means no metamethods (L == NULL in C).
959pub(crate) fn equal_obj(
960    state: Option<&mut LuaState>,
961    t1: &LuaValue,
962    t2: &LuaValue,
963) -> Result<bool, LuaError> {
964    // In Rust, same variant = same tag.  If variant differs, check the number
965    // special case (Int and Float can be equal).
966    let same_variant = std::mem::discriminant(t1) == std::mem::discriminant(t2);
967    if !same_variant {
968        let t1_is_num = matches!(t1, LuaValue::Int(_) | LuaValue::Float(_));
969        let t2_is_num = matches!(t2, LuaValue::Int(_) | LuaValue::Float(_));
970        if !(t1_is_num && t2_is_num) {
971            return Ok(false);
972        }
973        // luaV_tointegerns(t1, &i1, F2Ieq) && luaV_tointegerns(t2, &i2, F2Ieq) && i1==i2
974        let i1 = to_integer_ns(t1, F2Imod::Eq);
975        let i2 = to_integer_ns(t2, F2Imod::Eq);
976        return Ok(i1.is_some() && i2.is_some() && i1 == i2);
977    }
978
979    match (t1, t2) {
980        (LuaValue::Nil,  LuaValue::Nil)  => Ok(true),
981        (LuaValue::Bool(b1), LuaValue::Bool(b2)) => Ok(b1 == b2),
982        (LuaValue::Int(i1), LuaValue::Int(i2)) => Ok(i1 == i2),
983        (LuaValue::Float(f1), LuaValue::Float(f2)) => Ok(f1 == f2),
984        (LuaValue::LightUserData(p1), LuaValue::LightUserData(p2)) => Ok(p1 == p2),
985        (LuaValue::Function(f1), LuaValue::Function(f2)) => {
986            use lua_types::closure::LuaClosure;
987            let same = match (f1, f2) {
988                (LuaClosure::Lua(a), LuaClosure::Lua(b)) => GcRef::ptr_eq(a, b),
989                (LuaClosure::C(a), LuaClosure::C(b)) => GcRef::ptr_eq(a, b),
990                (LuaClosure::LightC(a), LuaClosure::LightC(b)) => a == b,
991                _ => false,
992            };
993            Ok(same)
994        }
995        (LuaValue::Str(s1), LuaValue::Str(s2)) => {
996            //    luaS_eqlngstr for long strings (content eq).
997            // In Rust, LuaString PartialEq handles both.
998            Ok(s1 == s2)
999        }
1000        (LuaValue::UserData(u1), LuaValue::UserData(u2)) => {
1001            //    else if (L == NULL) return 0;
1002            //    tm = fasttm(L, uvalue(t1)->metatable, TM_EQ);
1003            if std::ptr::eq(u1.as_ptr(), u2.as_ptr()) {
1004                return Ok(true);
1005            }
1006            let Some(state) = state else { return Ok(false); };
1007            let tm1 = state.fast_tm_ud(u1, TagMethod::Eq);
1008            let tm = if matches!(tm1, LuaValue::Nil) {
1009                state.fast_tm_ud(u2, TagMethod::Eq)
1010            } else {
1011                tm1
1012            };
1013            if matches!(tm, LuaValue::Nil) {
1014                return Ok(false);
1015            }
1016            let result = state.call_tm_res_bool(tm, t1, t2)?;
1017            Ok(result)
1018        }
1019        (LuaValue::Table(h1), LuaValue::Table(h2)) => {
1020            if std::ptr::eq(h1.as_ptr(), h2.as_ptr()) {
1021                return Ok(true);
1022            }
1023            let Some(state) = state else { return Ok(false); };
1024            //    if (tm == NULL) tm = fasttm(L, hvalue(t2)->metatable, TM_EQ);
1025            let mt1 = h1.metatable();
1026            let mt2 = h2.metatable();
1027            let tm1 = state.fast_tm_table(mt1.as_ref(), TagMethod::Eq);
1028            let tm = if matches!(tm1, LuaValue::Nil) {
1029                state.fast_tm_table(mt2.as_ref(), TagMethod::Eq)
1030            } else {
1031                tm1
1032            };
1033            if matches!(tm, LuaValue::Nil) {
1034                return Ok(false);
1035            }
1036            let result = state.call_tm_res_bool(tm, t1, t2)?;
1037            Ok(result)
1038        }
1039        (LuaValue::Thread(a), LuaValue::Thread(b)) => Ok(GcRef::ptr_eq(a, b)),
1040        _ => Ok(std::ptr::eq(t1 as *const _, t2 as *const _)),
1041    }
1042}
1043
1044// ─── Concatenation ───────────────────────────────────────────────────────────
1045
1046/// Copy `n` strings from `top-n .. top-1` into `buff`.
1047fn copy_to_buf(state: &LuaState, top: StackIdx, n: u32, buf: &mut Vec<u8>) {
1048    buf.clear();
1049    let mut remaining = n;
1050    loop {
1051        let idx = top - remaining as i32;
1052        let v = state.get_at(idx);
1053        if let LuaValue::Str(ts) = v {
1054            buf.extend_from_slice(ts.as_bytes());
1055        }
1056        if remaining <= 1 {
1057            break;
1058        }
1059        remaining -= 1;
1060    }
1061}
1062
1063/// Concatenate `total` values on the top of the stack, leaving one result.
1064pub(crate) fn concat(state: &mut LuaState, total: i32) -> Result<(), LuaError> {
1065    if total == 1 {
1066        return Ok(());
1067    }
1068    let mut total = total;
1069    loop {
1070        let top = state.top_idx();
1071        let v_tm1 = state.get_at(top - 1); // top-1
1072        let v_tm2 = state.get_at(top - 2); // top-2
1073
1074        //    luaT_tryconcatTM(L);
1075        let top2_coercible = matches!(v_tm2, LuaValue::Str(_))
1076            || matches!(v_tm2, LuaValue::Int(_) | LuaValue::Float(_));
1077        // tostring converts numbers to strings; we check top-1 too
1078        let top1_stringlike = matches!(v_tm1, LuaValue::Str(_))
1079            || matches!(v_tm1, LuaValue::Int(_) | LuaValue::Float(_));
1080        if !top2_coercible || !top1_stringlike {
1081            state.try_concat_tm(&v_tm1, &v_tm2)?;
1082            // at the bottom of the do-while runs for this branch too.
1083            // The metamethod writes its single result to top-2, leaving
1084            // top-1 stale; popping that stale slot is what makes the next
1085            // iteration see the just-computed result at the new top-1.
1086            total -= 1;
1087            let top = state.top_idx();
1088            state.set_top(top - 1);
1089            if total <= 1 {
1090                break;
1091            }
1092            continue;
1093        }
1094
1095        let is_empty = |v: &LuaValue| -> bool {
1096            matches!(v, LuaValue::Str(s) if s.as_bytes().is_empty())
1097        };
1098
1099        let n: u32;
1100        if is_empty(&v_tm1) {
1101            state.coerce_to_string(top - 2)?;
1102            n = 2;
1103        } else if is_empty(&v_tm2) {
1104            // so top-1 is guaranteed to be a string here. We replicate that
1105            // conversion before the copy so numbers don't leak through.
1106            state.coerce_to_string(top - 1)?;
1107            let v = state.get_at(top - 1);
1108            state.set_at(top - 2, v);
1109            n = 2;
1110        } else {
1111            // Ensure top-1 is a string (coerce if number)
1112            state.coerce_to_string(top - 1)?;
1113            let s1 = match state.get_at(top - 1) {
1114                LuaValue::Str(ts) => ts.as_bytes().len(),
1115                _ => 0,
1116            };
1117            let mut total_len = s1;
1118            let mut count: u32 = 1;
1119            let top = state.top_idx();
1120            loop {
1121                if count as i32 >= total {
1122                    break;
1123                }
1124                let idx = top - (count as i32 + 1);
1125                let v = state.get_at(idx);
1126                if !matches!(v, LuaValue::Str(_) | LuaValue::Int(_) | LuaValue::Float(_)) {
1127                    break;
1128                }
1129                state.coerce_to_string(idx)?;
1130                let l = match state.get_at(idx) {
1131                    LuaValue::Str(ts) => ts.as_bytes().len(),
1132                    _ => 0,
1133                };
1134                if l >= usize::MAX - total_len {
1135                    // pop strings to avoid wasting stack
1136                    state.set_top(top - total as i32);
1137                    return Err(LuaError::runtime(format_args!("string length overflow")));
1138                }
1139                total_len += l;
1140                count += 1;
1141            }
1142            n = count;
1143
1144            // Build concatenated result
1145            let mut buf: Vec<u8> = Vec::with_capacity(total_len);
1146            let top = state.top_idx();
1147            copy_to_buf(state, top, n, &mut buf);
1148            let ts = state.intern_or_create_str(&buf)?;
1149            state.set_at(top - n as i32, LuaValue::Str(ts));
1150        }
1151        total -= n as i32 - 1;
1152        let top = state.top_idx();
1153        state.set_top(top - ((n - 1) as i32));
1154
1155        if total <= 1 {
1156            break;
1157        }
1158    }
1159    Ok(())
1160}
1161
1162// ─── Object length ───────────────────────────────────────────────────────────
1163
1164/// Main implementation of the `#` operator.
1165pub(crate) fn obj_len(state: &mut LuaState, ra: StackIdx, rb: LuaValue) -> Result<(), LuaError> {
1166    match &rb {
1167        LuaValue::Table(_) => {
1168            //    if (tm) break; else setivalue(s2v(ra), luaH_getn(h));
1169            let mt = state.table_metatable(&rb);
1170            let tm = state.fast_tm_table(mt.as_ref(), TagMethod::Len);
1171            if matches!(tm, LuaValue::Nil) {
1172                let n = state.table_length(&rb)?;
1173                state.set_at(ra, LuaValue::Int(n as i64));
1174                return Ok(());
1175            }
1176            // Fall through to call metamethod
1177            state.call_tm_res(tm, &rb, &rb, ra)?;
1178        }
1179        LuaValue::Str(ts) => {
1180            //    case LUA_VLNGSTR: setivalue(s2v(ra), tsvalue(rb)->u.lnglen);
1181            // Unified in Rust — just get length
1182            let n = ts.len();
1183            state.set_at(ra, LuaValue::Int(n as i64));
1184        }
1185        other => {
1186            //    if (notm(tm)) luaG_typeerror(L, rb, "get length of");
1187            let tm = state.get_tm_by_obj(other, TagMethod::Len);
1188            if matches!(tm, LuaValue::Nil) {
1189                return Err(LuaError::type_error(other, "get length of"));
1190            }
1191            state.call_tm_res(tm, &rb, &rb, ra)?;
1192        }
1193    }
1194    Ok(())
1195}
1196
1197// ─── Integer arithmetic ──────────────────────────────────────────────────────
1198
1199/// Integer floor-division.
1200pub(crate) fn idiv(m: i64, n: i64) -> Result<i64, LuaError> {
1201    if (n as u64).wrapping_add(1) <= 1 {
1202        if n == 0 {
1203            return Err(LuaError::runtime(format_args!("attempt to divide by zero")));
1204        }
1205        return Ok(intop_sub(0, m));
1206    }
1207    let q = m / n;
1208    // Correct toward floor (C division truncates toward zero)
1209    if (m ^ n) < 0 && m % n != 0 {
1210        Ok(q - 1)
1211    } else {
1212        Ok(q)
1213    }
1214}
1215
1216/// Integer modulus (Lua semantics: same sign as divisor).
1217pub(crate) fn imod(m: i64, n: i64) -> Result<i64, LuaError> {
1218    if (n as u64).wrapping_add(1) <= 1 {
1219        if n == 0 {
1220            return Err(LuaError::runtime(format_args!("attempt to perform 'n%0'")));
1221        }
1222        return Ok(0);
1223    }
1224    let r = m % n;
1225    if r != 0 && (r ^ n) < 0 {
1226        Ok(r + n)
1227    } else {
1228        Ok(r)
1229    }
1230}
1231
1232/// Float modulus (Lua semantics).
1233pub(crate) fn fmodf(m: f64, n: f64) -> f64 {
1234    let r = m % n;
1235    let opposite_signs = if r > 0.0 { n < 0.0 } else { r < 0.0 && n > 0.0 };
1236    if opposite_signs {
1237        r + n
1238    } else {
1239        r
1240    }
1241}
1242
1243/// Phase-B helper: map a u8 raw value to a `TagMethod`. Mirrors C's
1244/// `cast(TMS, x)` direct cast; out-of-range returns `TagMethod::Index`.
1245pub(crate) fn tagmethod_from_index(i: usize) -> TagMethod {
1246    use TagMethod::*;
1247    match i {
1248        0 => Index, 1 => NewIndex, 2 => Gc, 3 => Mode, 4 => Len, 5 => Eq,
1249        6 => Add, 7 => Sub, 8 => Mul, 9 => Mod, 10 => Pow, 11 => Div,
1250        12 => Idiv, 13 => Band, 14 => Bor, 15 => Bxor, 16 => Shl, 17 => Shr,
1251        18 => Unm, 19 => Bnot, 20 => Lt, 21 => Le, 22 => Concat, 23 => Call,
1252        24 => Close,
1253        _ => Index,
1254    }
1255}
1256
1257/// Integer floor-mod: Lua's `%` operator on integers. Result has the same sign
1258/// as the divisor. Raises on `n == 0`.
1259pub(crate) fn int_floor_mod(_state: &mut LuaState, a: i64, b: i64) -> Result<i64, LuaError> {
1260    imod(a, b)
1261}
1262
1263/// Integer floor-div: Lua's `//` operator on integers. Truncates toward
1264/// negative infinity. Raises on `n == 0`.
1265pub(crate) fn int_floor_div(_state: &mut LuaState, a: i64, b: i64) -> Result<i64, LuaError> {
1266    idiv(a, b)
1267}
1268
1269/// Float floor-mod: Lua's `%` operator on floats. Result has the same sign as
1270/// the divisor.  NaN / division-by-zero behavior mirrors C `fmod`.
1271pub(crate) fn float_floor_mod(_state: &mut LuaState, a: f64, b: f64) -> Result<f64, LuaError> {
1272    Ok(fmodf(a, b))
1273}
1274
1275/// Left shift; right shift is shift-left by negated count.
1276pub(crate) fn shiftl(x: i64, y: i64) -> i64 {
1277    if y < 0 {
1278        if y <= -(NBITS as i64) {
1279            0
1280        } else {
1281            intop_shr(x, (-y) as u32)
1282        }
1283    } else {
1284        if y >= NBITS as i64 {
1285            0
1286        } else {
1287            intop_shl(x, y as u32)
1288        }
1289    }
1290}
1291
1292// ─── Closure creation ────────────────────────────────────────────────────────
1293
1294///                              StkId base, StkId ra)`
1295/// Create a new Lua closure from prototype `p`, initialise its upvalues,
1296/// and push it onto the stack at `ra`.
1297fn push_closure(
1298    state: &mut LuaState,
1299    proto_idx: usize,   // index into current closure's proto.p[]
1300    ci: CallInfoIdx,
1301    base: StackIdx,
1302    ra: StackIdx,
1303) -> Result<(), LuaError> {
1304    // TODO(port): pushclosure needs access to the enclosing closure's upvals and
1305    // the child proto from the current frame.  This stub forwards to a LuaState
1306    // method that has the required context.
1307    state.push_closure(proto_idx, ci, base, ra)
1308}
1309
1310// ─── Yield recovery ──────────────────────────────────────────────────────────
1311
1312/// Resume the opcode that was interrupted by a yield.
1313/// Called when a coroutine is resumed after yielding mid-instruction.
1314pub(crate) fn finish_op(state: &mut LuaState) -> Result<(), LuaError> {
1315    //    StkId base = ci->func.p + 1;
1316    //    Instruction inst = *(ci->u.l.savedpc - 1);
1317    //    OpCode op = GET_OPCODE(inst);
1318    let ci = state.current_ci_idx();
1319    let base = state.ci_base(ci);
1320    let inst = state.ci_prev_instruction(ci);
1321    let op = inst.opcode();
1322
1323    match op {
1324        //    setobjs2s(L, base + GETARG_A(*(ci->u.l.savedpc - 2)), --L->top.p);
1325        OpCode::MmBin | OpCode::MmBinI | OpCode::MmBinK => {
1326            let prev_inst = state.ci_prev2_instruction(ci);
1327            let a = prev_inst.arg_a();
1328            state.dec_top();
1329            let top = state.top_idx();
1330            let v = state.get_at(top);
1331            state.set_at(base + a, v);
1332        }
1333        //    setobjs2s(L, base + GETARG_A(inst), --L->top.p);
1334        OpCode::Unm | OpCode::BNot | OpCode::Len
1335        | OpCode::GetTabUp | OpCode::GetTable | OpCode::GetI
1336        | OpCode::GetField | OpCode::Self_ => {
1337            let a = inst.arg_a();
1338            state.dec_top();
1339            let top = state.top_idx();
1340            let v = state.get_at(top);
1341            state.set_at(base + a, v);
1342        }
1343        //    case OP_GTI: case OP_GEI: case OP_EQ:
1344        //    int res = !l_isfalse(s2v(L->top.p - 1)); L->top.p--;
1345        //    if (res != GETARG_k(inst)) ci->u.l.savedpc++;
1346        OpCode::Lt | OpCode::Le | OpCode::LtI | OpCode::LeI
1347        | OpCode::GtI | OpCode::GeI | OpCode::Eq => {
1348            let top_minus1 = state.top_idx() - 1;
1349            let v = state.get_at(top_minus1);
1350            let res = !matches!(v, LuaValue::Nil | LuaValue::Bool(false));
1351            state.dec_top();
1352            if (res as i32) != inst.arg_k() {
1353                state.ci_skip_next_instruction(ci);
1354            }
1355            // Note: CIST_LEQ compatibility not supported (LUA_COMPAT_LT_LE dropped)
1356        }
1357        //    StkId top = L->top.p - 1;
1358        //    int a = GETARG_A(inst);
1359        //    int total = cast_int(top - 1 - (base + a));
1360        //    setobjs2s(L, top - 2, top);  L->top.p = top - 1;
1361        //    luaV_concat(L, total);
1362        OpCode::Concat => {
1363            let top = state.top_idx() - 1; // top when luaT_tryconcatTM was called
1364            let a = inst.arg_a();
1365            let total_concat = (top - 1 - (base + a)) as i32;
1366            let v = state.get_at(top);
1367            state.set_at(top - 2, v);
1368            state.set_top(top - 1);
1369            concat(state, total_concat)?;
1370        }
1371        OpCode::Close => {
1372            state.ci_step_pc_back(ci);
1373        }
1374        //    StkId ra = base + GETARG_A(inst);
1375        //    L->top.p = ra + ci->u2.nres;
1376        //    ci->u.l.savedpc--;
1377        OpCode::Return => {
1378            let a = inst.arg_a();
1379            let ra = base + a;
1380            let nres = state.ci_nres(ci);
1381            state.set_top(ra + nres);
1382            state.ci_step_pc_back(ci);
1383        }
1384        other => {
1385            debug_assert!(
1386                matches!(
1387                    other,
1388                    OpCode::TForCall | OpCode::Call | OpCode::TailCall
1389                    | OpCode::SetTabUp | OpCode::SetTable | OpCode::SetI | OpCode::SetField
1390                ),
1391                "unexpected opcode in finish_op: {:?}",
1392                other
1393            );
1394        }
1395    }
1396    Ok(())
1397}
1398
1399// ─── Main interpreter loop ───────────────────────────────────────────────────
1400
1401/// Main Lua bytecode interpreter loop.
1402///
1403/// # Control flow modelling
1404/// The C function uses goto labels: `startfunc`, `returning`, `ret`,
1405/// `l_tforcall`, `l_tforloop`.  These are modelled as follows:
1406/// - `'startfunc: loop { ... }` — outer loop; `continue 'startfunc` = goto startfunc
1407/// - `'returning: loop { ... }` — inner loop; `continue 'returning` = goto returning
1408/// - `break 'dispatch` from the inner dispatch loop → runs `ret:` logic
1409/// - `l_tforcall` / `l_tforloop` — inlined at TFORPREP / TFORCALL handlers
1410pub(crate) fn execute(state: &mut LuaState, mut ci: CallInfoIdx) -> Result<(), LuaError> {
1411    let mut trap: bool;
1412
1413    // PORT NOTE: `startfunc:` is the entry point that (re)sets `trap`.
1414    'startfunc: loop {
1415        trap = state.hook_mask() != 0;
1416
1417        // PORT NOTE: `returning:` is the re-entry after a Lua call returns.
1418        // Re-enters 'returning without resetting trap.
1419        'returning: loop {
1420            let cl = match state.ci_lua_closure(ci) {
1421                Some(c) => c,
1422                None => {
1423                    return Err(LuaError::runtime(format_args!(
1424                        "internal: execute called on non-Lua frame"
1425                    )));
1426                }
1427            };
1428            // pc is an index into proto.code (u32)
1429            let mut pc: u32 = state.ci_savedpc(ci);
1430
1431            if trap {
1432                trap = state.trace_call(ci)?;
1433            }
1434            let mut base: StackIdx = state.ci_base(ci);
1435
1436            // ── Main dispatch loop ──────────────────────────────────────────
1437            'dispatch: loop {
1438                if trap {
1439                    trap = state.trace_exec(ci, pc)?;
1440                    base = state.ci_base(ci); // updatebase
1441                }
1442                let i: Instruction = state.proto_code(&cl, pc);
1443                pc += 1;
1444                let op = i.opcode();
1445
1446                debug_assert!(base == state.ci_base(ci));
1447
1448                // In normal C-Lua builds, `lua_assert` compiles away; keep the
1449                // stack-top invalidation only for debug parity so release
1450                // dispatch avoids an opcode-mode lookup and a `top` write.
1451                #[cfg(debug_assertions)]
1452                {
1453                    let op_mode = op_mode_byte(op);
1454                    if (op_mode & (1 << 5)) == 0 || i.arg_b() != 0 {
1455                        state.set_top(base);
1456                    }
1457                }
1458
1459                match op {
1460                    // ── OP_MOVE ──────────────────────────────────────────────
1461                    OpCode::Move => {
1462                        let ra = base + i.arg_a();
1463                        let rb = base + i.arg_b();
1464                        let v = state.stack[rb.0 as usize].val;
1465                        state.stack[ra.0 as usize].val = v;
1466                    }
1467                    // ── OP_LOADI ─────────────────────────────────────────────
1468                    OpCode::LoadI => {
1469                        let ra = base + i.arg_a();
1470                        let b = i.arg_s_bx() as i64;
1471                        state.set_at(ra, LuaValue::Int(b));
1472                    }
1473                    // ── OP_LOADF ─────────────────────────────────────────────
1474                    OpCode::LoadF => {
1475                        let ra = base + i.arg_a();
1476                        let b = i.arg_s_bx() as f64;
1477                        state.set_at(ra, LuaValue::Float(b));
1478                    }
1479                    // ── OP_LOADK ─────────────────────────────────────────────
1480                    OpCode::LoadK => {
1481                        let ra = base + i.arg_a();
1482                        let k_idx = i.arg_bx() as usize;
1483                        let v = state.proto_const(&cl, k_idx).clone();
1484                        state.set_at(ra, v);
1485                    }
1486                    // ── OP_LOADKX ────────────────────────────────────────────
1487                    OpCode::LoadKX => {
1488                        let ra = base + i.arg_a();
1489                        let extra = state.proto_code(&cl, pc);
1490                        pc += 1;
1491                        let k_idx = extra.arg_ax() as usize;
1492                        let v = state.proto_const(&cl, k_idx).clone();
1493                        state.set_at(ra, v);
1494                    }
1495                    // ── OP_LOADFALSE ─────────────────────────────────────────
1496                    OpCode::LoadFalse => {
1497                        let ra = base + i.arg_a();
1498                        state.set_at(ra, LuaValue::Bool(false));
1499                    }
1500                    // ── OP_LFALSESKIP ────────────────────────────────────────
1501                    OpCode::LFalseSkip => {
1502                        let ra = base + i.arg_a();
1503                        state.set_at(ra, LuaValue::Bool(false));
1504                        pc += 1;
1505                    }
1506                    // ── OP_LOADTRUE ──────────────────────────────────────────
1507                    OpCode::LoadTrue => {
1508                        let ra = base + i.arg_a();
1509                        state.set_at(ra, LuaValue::Bool(true));
1510                    }
1511                    // ── OP_LOADNIL ───────────────────────────────────────────
1512                    OpCode::LoadNil => {
1513                        let ra = base + i.arg_a();
1514                        let b = i.arg_b();
1515                        for k in 0..=b {
1516                            state.set_at(ra + k, LuaValue::Nil);
1517                        }
1518                    }
1519                    // ── OP_GETUPVAL ──────────────────────────────────────────
1520                    OpCode::GetUpVal => {
1521                        let ra = base + i.arg_a();
1522                        let b = i.arg_b() as usize;
1523                        let v = state.upvalue_get(&cl, b);
1524                        state.set_at(ra, v);
1525                    }
1526                    // ── OP_SETUPVAL ──────────────────────────────────────────
1527                    //    setobj(L, uv->v.p, s2v(ra)); luaC_barrier(L, uv, s2v(ra));
1528                    OpCode::SetUpVal => {
1529                        let ra = base + i.arg_a();
1530                        let b = i.arg_b() as usize;
1531                        let v = state.stack[ra.0 as usize].val;
1532                        let uv = cl.upval(b);
1533                        match uv.try_open_payload() {
1534                            Some((thread_id, idx)) if thread_id as u64 == state.cached_thread_id => {
1535                                state.stack[idx.0 as usize].val = v;
1536                            }
1537                            _ => {
1538                                state.upvalue_set(&cl, b, v)?;
1539                            }
1540                        }
1541                    }
1542                    // ── OP_GETTABUP ──────────────────────────────────────────
1543                    //    if (luaV_fastget(..., luaH_getshortstr)) setobj2s(L, ra, slot)
1544                    //    else Protect(luaV_finishget(...))
1545                    OpCode::GetTabUp => {
1546                        let ra = base + i.arg_a();
1547                        let b = i.arg_b() as usize;
1548                        let k_idx = i.arg_c() as usize;
1549                        let upval = state.upvalue_get(&cl, b);
1550                        let key = state.proto_const(&cl, k_idx).clone();
1551                        match state.fast_get_short_str(&upval, &key)? {
1552                            Some(v) => state.set_at(ra, v),
1553                            None => {
1554                                state.set_ci_savedpc(ci, pc);
1555                                state.set_top(state.ci_top(ci));
1556                                finish_get(state, upval, key, ra, true, None)?;
1557                                trap = state.ci_trap(ci);
1558                            }
1559                        }
1560                    }
1561                    // ── OP_GETTABLE ──────────────────────────────────────────
1562                    //    if (integer key) fastgeti else fastget
1563                    OpCode::GetTable => {
1564                        let ra = base + i.arg_a();
1565                        let rb_idx = base + i.arg_b();
1566                        let rb_v = state.get_at(rb_idx);
1567                        let rc_v = state.get_at(base + i.arg_c());
1568                        let fast_result = if let LuaValue::Int(n) = &rc_v {
1569                            state.fast_get_int(&rb_v, *n)?
1570                        } else {
1571                            state.fast_get(&rb_v, &rc_v)?
1572                        };
1573                        match fast_result {
1574                            Some(v) => state.set_at(ra, v),
1575                            None => {
1576                                state.set_ci_savedpc(ci, pc);
1577                                state.set_top(state.ci_top(ci));
1578                                finish_get(state, rb_v, rc_v, ra, true, Some(rb_idx))?;
1579                                trap = state.ci_trap(ci);
1580                            }
1581                        }
1582                    }
1583                    // ── OP_GETI ──────────────────────────────────────────────
1584                    //    if (luaV_fastgeti(L, rb, c, slot)) setobj2s(L, ra, slot)
1585                    //    else { TValue key; setivalue(&key, c); Protect(finishget) }
1586                    OpCode::GetI => {
1587                        let ra = base + i.arg_a();
1588                        let rb_idx = base + i.arg_b();
1589                        let rb_v = state.get_at(rb_idx);
1590                        let c = i.arg_c() as i64;
1591                        match state.fast_get_int(&rb_v, c)? {
1592                            Some(v) => state.set_at(ra, v),
1593                            None => {
1594                                let key = LuaValue::Int(c);
1595                                state.set_ci_savedpc(ci, pc);
1596                                state.set_top(state.ci_top(ci));
1597                                finish_get(state, rb_v, key, ra, true, Some(rb_idx))?;
1598                                trap = state.ci_trap(ci);
1599                            }
1600                        }
1601                    }
1602                    // ── OP_GETFIELD ──────────────────────────────────────────
1603                    OpCode::GetField => {
1604                        let ra = base + i.arg_a();
1605                        let rb_idx = base + i.arg_b();
1606                        let rb_v = state.get_at(rb_idx);
1607                        let k_idx = i.arg_c() as usize;
1608                        let key = state.proto_const(&cl, k_idx).clone();
1609                        match state.fast_get_short_str(&rb_v, &key)? {
1610                            Some(v) => state.set_at(ra, v),
1611                            None => {
1612                                state.set_ci_savedpc(ci, pc);
1613                                state.set_top(state.ci_top(ci));
1614                                finish_get(state, rb_v, key, ra, true, Some(rb_idx))?;
1615                                trap = state.ci_trap(ci);
1616                            }
1617                        }
1618                    }
1619                    // ── OP_SETTABUP ──────────────────────────────────────────
1620                    OpCode::SetTabUp => {
1621                        let a = i.arg_a() as usize;
1622                        let b_idx = i.arg_b() as usize; // key is KB(i)
1623                        let rc_v = if i.test_k() {
1624                            state.proto_const(&cl, i.arg_c() as usize).clone()
1625                        } else {
1626                            state.get_at(base + i.arg_c())
1627                        };
1628                        let upval = state.upvalue_get(&cl, a);
1629                        let key = state.proto_const(&cl, b_idx).clone();
1630                        match state.fast_get_short_str(&upval, &key)? {
1631                            Some(_slot) => {
1632                                state.table_raw_set(&upval, key, rc_v.clone())?;
1633                                state.gc_barrier_back(&upval, &rc_v);
1634                            }
1635                            None => {
1636                                state.set_ci_savedpc(ci, pc);
1637                                state.set_top(state.ci_top(ci));
1638                                let upval_name: Vec<u8> = cl
1639                                    .proto
1640                                    .upvalues
1641                                    .get(a)
1642                                    .and_then(|uv| uv.name.as_ref())
1643                                    .map(|s| s.as_bytes().to_vec())
1644                                    .unwrap_or_else(|| b"?".to_vec());
1645                                let hint: Option<(&[u8], &[u8])> =
1646                                    Some((b"upvalue", &upval_name));
1647                                finish_set(state, upval, key, rc_v, false, None, hint)?;
1648                                trap = state.ci_trap(ci);
1649                            }
1650                        }
1651                    }
1652                    // ── OP_SETTABLE ───────────────────────────────────────────
1653                    OpCode::SetTable => {
1654                        let ra_idx = base + i.arg_a();
1655                        let ra_v = state.get_at(ra_idx);
1656                        let rb_v = state.get_at(base + i.arg_b());
1657                        let rc_v = if i.test_k() {
1658                            state.proto_const(&cl, i.arg_c() as usize).clone()
1659                        } else {
1660                            state.get_at(base + i.arg_c())
1661                        };
1662                        let fast = if let LuaValue::Int(n) = &rb_v {
1663                            state.fast_get_int(&ra_v, *n)?
1664                        } else {
1665                            state.fast_get(&ra_v, &rb_v)?
1666                        };
1667                        if fast.is_some() {
1668                            state.table_raw_set(&ra_v, rb_v, rc_v.clone())?;
1669                            state.gc_barrier_back(&ra_v, &rc_v);
1670                        } else {
1671                            state.set_ci_savedpc(ci, pc);
1672                            state.set_top(state.ci_top(ci));
1673                            finish_set(state, ra_v, rb_v, rc_v, false, Some(ra_idx), None)?;
1674                            trap = state.ci_trap(ci);
1675                        }
1676                    }
1677                    // ── OP_SETI ───────────────────────────────────────────────
1678                    OpCode::SetI => {
1679                        let ra_idx = base + i.arg_a();
1680                        let ra_v = state.get_at(ra_idx);
1681                        let c = i.arg_b() as i64;
1682                        let rc_v = if i.test_k() {
1683                            state.proto_const(&cl, i.arg_c() as usize).clone()
1684                        } else {
1685                            state.get_at(base + i.arg_c())
1686                        };
1687                        let fast = state.fast_get_int(&ra_v, c)?;
1688                        if fast.is_some() {
1689                            state.table_raw_set(&ra_v, LuaValue::Int(c), rc_v.clone())?;
1690                            state.gc_barrier_back(&ra_v, &rc_v);
1691                        } else {
1692                            state.set_ci_savedpc(ci, pc);
1693                            state.set_top(state.ci_top(ci));
1694                            finish_set(state, ra_v, LuaValue::Int(c), rc_v, false, Some(ra_idx), None)?;
1695                            trap = state.ci_trap(ci);
1696                        }
1697                    }
1698                    // ── OP_SETFIELD ───────────────────────────────────────────
1699                    OpCode::SetField => {
1700                        let ra_idx = base + i.arg_a();
1701                        let ra_v = state.get_at(ra_idx);
1702                        let b_idx = i.arg_b() as usize;
1703                        let key = state.proto_const(&cl, b_idx).clone();
1704                        let rc_v = if i.test_k() {
1705                            state.proto_const(&cl, i.arg_c() as usize).clone()
1706                        } else {
1707                            state.get_at(base + i.arg_c())
1708                        };
1709                        match state.fast_get_short_str(&ra_v, &key)? {
1710                            Some(_) => {
1711                                state.table_raw_set(&ra_v, key, rc_v.clone())?;
1712                                state.gc_barrier_back(&ra_v, &rc_v);
1713                            }
1714                            None => {
1715                                state.set_ci_savedpc(ci, pc);
1716                                state.set_top(state.ci_top(ci));
1717                                finish_set(state, ra_v, key, rc_v, false, Some(ra_idx), None)?;
1718                                trap = state.ci_trap(ci);
1719                            }
1720                        }
1721                    }
1722                    // ── OP_NEWTABLE ───────────────────────────────────────────
1723                    //    if (TESTARG_k(i)) c += GETARG_Ax(*pc) * (MAXARG_C + 1); pc++;
1724                    OpCode::NewTable => {
1725                        let ra = base + i.arg_a();
1726                        let mut b = i.arg_b();
1727                        let mut c = i.arg_c();
1728                        if b > 0 {
1729                            b = 1 << (b - 1);
1730                        }
1731                        if i.test_k() {
1732                            let extra = state.proto_code(&cl, pc);
1733                            pc += 1;
1734                            const MAXARG_C: i32 = (1 << 8) - 1;
1735                            c += extra.arg_ax() * (MAXARG_C + 1);
1736                        } else {
1737                            pc += 1; // skip extra argument even if zero
1738                        }
1739                        state.set_top(ra + 1);
1740                        let t = if b != 0 || c != 0 {
1741                            state.new_table_with_sizes(c as u32, b as u32)?
1742                        } else {
1743                            state.new_table()
1744                        };
1745                        state.set_at(ra, LuaValue::Table(t.clone()));
1746                        state.set_ci_savedpc(ci, pc);
1747                        state.set_top(ra + 1);
1748                        state.gc_cond_step();
1749                        if state.hookmask != 0 {
1750                            trap = state.ci_trap(ci);
1751                        }
1752                    }
1753                    // ── OP_SELF ───────────────────────────────────────────────
1754                    OpCode::Self_ => {
1755                        let ra = base + i.arg_a();
1756                        let rb_idx = base + i.arg_b();
1757                        let rb_v = state.get_at(rb_idx);
1758                        let k_idx = i.arg_c() as usize; // RKC key (always a string)
1759                        let key = if i.test_k() {
1760                            state.proto_const(&cl, k_idx).clone()
1761                        } else {
1762                            state.get_at(base + i.arg_c())
1763                        };
1764                        state.set_at(ra + 1, rb_v.clone());
1765                        match state.fast_get_short_str(&rb_v, &key)? {
1766                            Some(v) => state.set_at(ra, v),
1767                            None => {
1768                                state.set_ci_savedpc(ci, pc);
1769                                state.set_top(state.ci_top(ci));
1770                                finish_get(state, rb_v, key, ra, true, Some(rb_idx))?;
1771                                trap = state.ci_trap(ci);
1772                            }
1773                        }
1774                    }
1775                    // ── Arithmetic immediates ──────────────────────────────────
1776                    OpCode::AddI => {
1777                        let ra = base + i.arg_a();
1778                        let rb = base + i.arg_b();
1779                        let imm = i.arg_s_c() as i64;
1780                        let rb_v = state.stack[rb.0 as usize].val;
1781                        match rb_v {
1782                            LuaValue::Int(iv1) => {
1783                                pc += 1;
1784                                state.stack[ra.0 as usize].val = LuaValue::Int(intop_add(iv1, imm));
1785                            }
1786                            LuaValue::Float(nb) => {
1787                                pc += 1;
1788                                state.stack[ra.0 as usize].val = LuaValue::Float(nb + imm as f64);
1789                            }
1790                            _ => {}
1791                        }
1792                    }
1793                    // ── Arithmetic with K constant operand ─────────────────────
1794                    OpCode::AddK => {
1795                        let ra = base + i.arg_a();
1796                        let rb = base + i.arg_b();
1797                        let kidx = i.arg_c() as usize;
1798                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
1799                            pc += 1;
1800                            state.set_at(ra, LuaValue::Int(intop_add(i1, i2)));
1801                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1802                            pc += 1;
1803                            state.set_at(ra, LuaValue::Float(n1 + n2));
1804                        }
1805                    }
1806                    OpCode::SubK => {
1807                        let ra = base + i.arg_a();
1808                        let rb = base + i.arg_b();
1809                        let kidx = i.arg_c() as usize;
1810                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
1811                            pc += 1;
1812                            state.set_at(ra, LuaValue::Int(intop_sub(i1, i2)));
1813                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1814                            pc += 1;
1815                            state.set_at(ra, LuaValue::Float(n1 - n2));
1816                        }
1817                    }
1818                    OpCode::MulK => {
1819                        let ra = base + i.arg_a();
1820                        let rb = base + i.arg_b();
1821                        let kidx = i.arg_c() as usize;
1822                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
1823                            pc += 1;
1824                            state.set_at(ra, LuaValue::Int(intop_mul(i1, i2)));
1825                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1826                            pc += 1;
1827                            state.set_at(ra, LuaValue::Float(n1 * n2));
1828                        }
1829                    }
1830                    OpCode::ModK => {
1831                        let ra = base + i.arg_a();
1832                        let v1 = state.get_at(base + i.arg_b());
1833                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1834                        state.set_ci_savedpc(ci, pc); // savestate for div-by-zero
1835                        state.set_top(state.ci_top(ci));
1836                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
1837                            |a, b| imod(a, b), fmodf)?;
1838                    }
1839                    OpCode::PowK => {
1840                        let ra = base + i.arg_a();
1841                        let rb = base + i.arg_b();
1842                        let kidx = i.arg_c() as usize;
1843                        if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1844                            pc += 1;
1845                            let r = if n2 == 2.0 { n1 * n1 } else { n1.powf(n2) };
1846                            state.set_at(ra, LuaValue::Float(r));
1847                        }
1848                    }
1849                    OpCode::DivK => {
1850                        let ra = base + i.arg_a();
1851                        let rb = base + i.arg_b();
1852                        let kidx = i.arg_c() as usize;
1853                        if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1854                            pc += 1;
1855                            state.set_at(ra, LuaValue::Float(n1 / n2));
1856                        }
1857                    }
1858                    OpCode::IDivK => {
1859                        let ra = base + i.arg_a();
1860                        let v1 = state.get_at(base + i.arg_b());
1861                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1862                        state.set_ci_savedpc(ci, pc);
1863                        state.set_top(state.ci_top(ci));
1864                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
1865                            |a, b| idiv(a, b), |a, b| (a / b).floor())?;
1866                    }
1867                    OpCode::BAndK => {
1868                        let ra = base + i.arg_a();
1869                        let v1 = state.get_at(base + i.arg_b());
1870                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1871                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_band);
1872                    }
1873                    OpCode::BOrK => {
1874                        let ra = base + i.arg_a();
1875                        let v1 = state.get_at(base + i.arg_b());
1876                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1877                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_bor);
1878                    }
1879                    OpCode::BXOrK => {
1880                        let ra = base + i.arg_a();
1881                        let v1 = state.get_at(base + i.arg_b());
1882                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1883                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_bxor);
1884                    }
1885                    OpCode::ShrI => {
1886                        let ra = base + i.arg_a();
1887                        let v = state.get_at(base + i.arg_b());
1888                        let ic = i.arg_s_c() as i64;
1889                        if let Some(ib) = to_integer_ns(&v, F2Imod::Eq) {
1890                            pc += 1;
1891                            state.set_at(ra, LuaValue::Int(shiftl(ib, -ic)));
1892                        }
1893                    }
1894                    OpCode::ShlI => {
1895                        let ra = base + i.arg_a();
1896                        let v = state.get_at(base + i.arg_b());
1897                        let ic = i.arg_s_c() as i64;
1898                        if let Some(ib) = to_integer_ns(&v, F2Imod::Eq) {
1899                            pc += 1;
1900                            state.set_at(ra, LuaValue::Int(shiftl(ic, ib)));
1901                        }
1902                    }
1903                    // ── Arithmetic with register operands ──────────────────────
1904                    OpCode::Add => {
1905                        let ra = base + i.arg_a();
1906                        let rb = base + i.arg_b();
1907                        let rc = base + i.arg_c();
1908                        let ra_u = ra.0 as usize;
1909                        let rb_v = state.stack[rb.0 as usize].val;
1910                        let rc_v = state.stack[rc.0 as usize].val;
1911                        if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (rb_v, rc_v) {
1912                            pc += 1;
1913                            state.stack[ra_u].val = LuaValue::Int(intop_add(i1, i2));
1914                        } else if let (Some(n1), Some(n2)) = (number_value(rb_v), number_value(rc_v)) {
1915                            pc += 1;
1916                            state.stack[ra_u].val = LuaValue::Float(n1 + n2);
1917                        }
1918                    }
1919                    OpCode::Sub => {
1920                        let ra = base + i.arg_a();
1921                        let rb = base + i.arg_b();
1922                        let rc = base + i.arg_c();
1923                        let ra_u = ra.0 as usize;
1924                        let rb_v = state.stack[rb.0 as usize].val;
1925                        let rc_v = state.stack[rc.0 as usize].val;
1926                        if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (rb_v, rc_v) {
1927                            pc += 1;
1928                            state.stack[ra_u].val = LuaValue::Int(intop_sub(i1, i2));
1929                        } else if let (Some(n1), Some(n2)) = (number_value(rb_v), number_value(rc_v)) {
1930                            pc += 1;
1931                            state.stack[ra_u].val = LuaValue::Float(n1 - n2);
1932                        }
1933                    }
1934                    OpCode::Mul => {
1935                        let ra = base + i.arg_a();
1936                        let rb = base + i.arg_b();
1937                        let rc = base + i.arg_c();
1938                        if let Some((i1, i2)) = state.get_int_pair_at(rb, rc) {
1939                            pc += 1;
1940                            state.set_at(ra, LuaValue::Int(intop_mul(i1, i2)));
1941                        } else if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
1942                            pc += 1;
1943                            state.set_at(ra, LuaValue::Float(n1 * n2));
1944                        }
1945                    }
1946                    OpCode::Mod => {
1947                        let ra = base + i.arg_a();
1948                        let v1 = state.get_at(base + i.arg_b());
1949                        let v2 = state.get_at(base + i.arg_c());
1950                        state.set_ci_savedpc(ci, pc);
1951                        state.set_top(state.ci_top(ci));
1952                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
1953                            |a, b| imod(a, b), fmodf)?;
1954                    }
1955                    OpCode::Pow => {
1956                        let ra = base + i.arg_a();
1957                        let rb = base + i.arg_b();
1958                        let rc = base + i.arg_c();
1959                        if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
1960                            pc += 1;
1961                            let r = if n2 == 2.0 { n1 * n1 } else { n1.powf(n2) };
1962                            state.set_at(ra, LuaValue::Float(r));
1963                        }
1964                    }
1965                    OpCode::Div => {
1966                        let ra = base + i.arg_a();
1967                        let rb = base + i.arg_b();
1968                        let rc = base + i.arg_c();
1969                        if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
1970                            pc += 1;
1971                            state.set_at(ra, LuaValue::Float(n1 / n2));
1972                        }
1973                    }
1974                    OpCode::IDiv => {
1975                        let ra = base + i.arg_a();
1976                        let v1 = state.get_at(base + i.arg_b());
1977                        let v2 = state.get_at(base + i.arg_c());
1978                        state.set_ci_savedpc(ci, pc);
1979                        state.set_top(state.ci_top(ci));
1980                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
1981                            |a, b| idiv(a, b), |a, b| (a / b).floor())?;
1982                    }
1983                    // ── Bitwise with register operands ─────────────────────────
1984                    // if (tointegerns(v1, &i1) && tointegerns(v2, &i2)) { pc++; setivalue... }
1985                    OpCode::BAnd => {
1986                        let ra = base + i.arg_a();
1987                        let v1 = state.get_at(base + i.arg_b());
1988                        let v2 = state.get_at(base + i.arg_c());
1989                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_band);
1990                    }
1991                    OpCode::BOr => {
1992                        let ra = base + i.arg_a();
1993                        let v1 = state.get_at(base + i.arg_b());
1994                        let v2 = state.get_at(base + i.arg_c());
1995                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_bor);
1996                    }
1997                    OpCode::BXOr => {
1998                        let ra = base + i.arg_a();
1999                        let v1 = state.get_at(base + i.arg_b());
2000                        let v2 = state.get_at(base + i.arg_c());
2001                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_bxor);
2002                    }
2003                    OpCode::Shr => {
2004                        let ra = base + i.arg_a();
2005                        let v1 = state.get_at(base + i.arg_b());
2006                        let v2 = state.get_at(base + i.arg_c());
2007                        bitwise_shift_rr(state, ra, &v1, &v2, &mut pc, true);
2008                    }
2009                    OpCode::Shl => {
2010                        let ra = base + i.arg_a();
2011                        let v1 = state.get_at(base + i.arg_b());
2012                        let v2 = state.get_at(base + i.arg_c());
2013                        bitwise_shift_rr(state, ra, &v1, &v2, &mut pc, false);
2014                    }
2015                    // ── OP_MMBIN ─────────────────────────────────────────────
2016                    // Instruction pi = *(pc - 2); TMS tm = (TMS)GETARG_C(i);
2017                    // StkId result = RA(pi);
2018                    // Protect(luaT_trybinTM(L, s2v(ra), rb, result, tm));
2019                    OpCode::MmBin => {
2020                        let ra_idx = base + i.arg_a();
2021                        let rb_idx = base + i.arg_b();
2022                        let ra_v = state.get_at(ra_idx);
2023                        let rb_v = state.get_at(rb_idx);
2024                        let tm = tagmethod_from_index(i.arg_c() as usize);
2025                        let prev_inst = state.proto_code(&cl, pc - 2);
2026                        let result_idx = base + prev_inst.arg_a();
2027                        state.set_ci_savedpc(ci, pc);
2028                        state.set_top(state.ci_top(ci));
2029                        state.try_bin_tm(&ra_v, Some(ra_idx), &rb_v, Some(rb_idx), result_idx, tm)?;
2030                        trap = state.ci_trap(ci);
2031                    }
2032                    OpCode::MmBinI => {
2033                        let ra_idx = base + i.arg_a();
2034                        let ra_v = state.get_at(ra_idx);
2035                        let imm = i.arg_s_b() as i64;
2036                        let tm = tagmethod_from_index(i.arg_c() as usize);
2037                        let flip = i.arg_k() != 0;
2038                        let prev_inst = state.proto_code(&cl, pc - 2);
2039                        let result_idx = base + prev_inst.arg_a();
2040                        state.set_ci_savedpc(ci, pc);
2041                        state.set_top(state.ci_top(ci));
2042                        state.try_bin_i_tm(&ra_v, Some(ra_idx), imm, flip, result_idx, tm)?;
2043                        trap = state.ci_trap(ci);
2044                    }
2045                    OpCode::MmBinK => {
2046                        let ra_idx = base + i.arg_a();
2047                        let ra_v = state.get_at(ra_idx);
2048                        let imm = state.proto_const(&cl, i.arg_b() as usize).clone();
2049                        let tm = tagmethod_from_index(i.arg_c() as usize);
2050                        let flip = i.arg_k() != 0;
2051                        let prev_inst = state.proto_code(&cl, pc - 2);
2052                        let result_idx = base + prev_inst.arg_a();
2053                        state.set_ci_savedpc(ci, pc);
2054                        state.set_top(state.ci_top(ci));
2055                        state.try_bin_assoc_tm(&ra_v, Some(ra_idx), &imm, None, flip, result_idx, tm)?;
2056                        trap = state.ci_trap(ci);
2057                    }
2058                    // ── OP_UNM ───────────────────────────────────────────────
2059                    //    else if (tonumberns(rb, nb)) setfltvalue(s2v(ra), -nb)
2060                    //    else Protect(luaT_trybinTM(L, rb, rb, ra, TM_UNM))
2061                    OpCode::Unm => {
2062                        let ra = base + i.arg_a();
2063                        let rb_idx = base + i.arg_b();
2064                        let rb_v = state.get_at(rb_idx);
2065                        match &rb_v {
2066                            LuaValue::Int(ib) => {
2067                                state.set_at(ra, LuaValue::Int(intop_sub(0, *ib)));
2068                            }
2069                            LuaValue::Float(nb) => {
2070                                state.set_at(ra, LuaValue::Float(-nb));
2071                            }
2072                            _ => {
2073                                state.set_ci_savedpc(ci, pc);
2074                                state.set_top(state.ci_top(ci));
2075                                state.try_bin_tm(&rb_v, Some(rb_idx), &rb_v, Some(rb_idx), ra, TagMethod::Unm)?;
2076                                trap = state.ci_trap(ci);
2077                            }
2078                        }
2079                    }
2080                    // ── OP_BNOT ──────────────────────────────────────────────
2081                    OpCode::BNot => {
2082                        let ra = base + i.arg_a();
2083                        let rb_idx = base + i.arg_b();
2084                        let rb_v = state.get_at(rb_idx);
2085                        if let Some(ib) = to_integer_ns(&rb_v, F2Imod::Eq) {
2086                            state.set_at(ra, LuaValue::Int(!ib));
2087                        } else {
2088                            state.set_ci_savedpc(ci, pc);
2089                            state.set_top(state.ci_top(ci));
2090                            state.try_bin_tm(&rb_v, Some(rb_idx), &rb_v, Some(rb_idx), ra, TagMethod::Bnot)?;
2091                            trap = state.ci_trap(ci);
2092                        }
2093                    }
2094                    // ── OP_NOT ───────────────────────────────────────────────
2095                    OpCode::Not => {
2096                        let ra = base + i.arg_a();
2097                        let rb_v = state.get_at(base + i.arg_b());
2098                        let falsy = matches!(rb_v, LuaValue::Nil | LuaValue::Bool(false));
2099                        state.set_at(ra, LuaValue::Bool(falsy));
2100                    }
2101                    // ── OP_LEN ───────────────────────────────────────────────
2102                    OpCode::Len => {
2103                        let ra = base + i.arg_a();
2104                        let rb_v = state.get_at(base + i.arg_b());
2105                        state.set_ci_savedpc(ci, pc);
2106                        state.set_top(state.ci_top(ci));
2107                        obj_len(state, ra, rb_v)?;
2108                        trap = state.ci_trap(ci);
2109                    }
2110                    // ── OP_CONCAT ─────────────────────────────────────────────
2111                    OpCode::Concat => {
2112                        let ra = base + i.arg_a();
2113                        let n = i.arg_b() as i32;
2114                        state.set_top(ra + n as i32);
2115                        state.set_ci_savedpc(ci, pc); // ProtectNT: save pc only
2116                        concat(state, n)?;
2117                        let top = state.top_idx();
2118                        state.set_ci_savedpc(ci, pc);
2119                        state.set_top(top);
2120                        state.gc_cond_step();
2121                        trap = state.ci_trap(ci);
2122                    }
2123                    // ── OP_CLOSE ──────────────────────────────────────────────
2124                    OpCode::Close => {
2125                        let ra = base + i.arg_a();
2126                        state.set_ci_savedpc(ci, pc);
2127                        state.set_top(state.ci_top(ci));
2128                        crate::func::close(state, ra, lua_types::status::LuaStatus::Ok as i32, true)?;
2129                        trap = state.ci_trap(ci);
2130                    }
2131                    // ── OP_TBC ────────────────────────────────────────────────
2132                    OpCode::Tbc => {
2133                        let ra = base + i.arg_a();
2134                        state.set_ci_savedpc(ci, pc);
2135                        state.set_top(state.ci_top(ci));
2136                        state.new_tbc_upval(ra)?;
2137                    }
2138                    // ── OP_JMP ────────────────────────────────────────────────
2139                    OpCode::Jmp => {
2140                        pc = (pc as i64 + i.arg_s_j() as i64) as u32;
2141                        trap = state.ci_trap(ci);
2142                    }
2143                    // ── OP_EQ ─────────────────────────────────────────────────
2144                    OpCode::Eq => {
2145                        let ra_v = state.get_at(base + i.arg_a());
2146                        let rb_v = state.get_at(base + i.arg_b());
2147                        state.set_ci_savedpc(ci, pc);
2148                        state.set_top(state.ci_top(ci));
2149                        let cond = equal_obj(Some(state), &ra_v, &rb_v)? as u32;
2150                        trap = state.ci_trap(ci);
2151                        if (cond as i32) != i.arg_k() {
2152                            pc += 1;
2153                        } else {
2154                            let next = state.proto_code(&cl, pc);
2155                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2156                            trap = state.ci_trap(ci);
2157                        }
2158                    }
2159                    // ── OP_LT ─────────────────────────────────────────────────
2160                    OpCode::Lt => {
2161                        let ra_v = state.get_at(base + i.arg_a());
2162                        let rb_v = state.get_at(base + i.arg_b());
2163                        let cond = if let (LuaValue::Int(ia), LuaValue::Int(ib)) = (&ra_v, &rb_v) {
2164                            *ia < *ib
2165                        } else if matches!((&ra_v, &rb_v),
2166                            (LuaValue::Int(_) | LuaValue::Float(_),
2167                             LuaValue::Int(_) | LuaValue::Float(_))) {
2168                            lt_num(&ra_v, &rb_v)
2169                        } else {
2170                            state.set_ci_savedpc(ci, pc);
2171                            state.set_top(state.ci_top(ci));
2172                            let r = less_than_others(state, &ra_v, &rb_v)?;
2173                            trap = state.ci_trap(ci);
2174                            r
2175                        };
2176                        if (cond as i32) != i.arg_k() {
2177                            pc += 1;
2178                        } else {
2179                            let next = state.proto_code(&cl, pc);
2180                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2181                            trap = state.ci_trap(ci);
2182                        }
2183                    }
2184                    // ── OP_LE ─────────────────────────────────────────────────
2185                    OpCode::Le => {
2186                        let ra_v = state.get_at(base + i.arg_a());
2187                        let rb_v = state.get_at(base + i.arg_b());
2188                        let cond = if let (LuaValue::Int(ia), LuaValue::Int(ib)) = (&ra_v, &rb_v) {
2189                            *ia <= *ib
2190                        } else if matches!((&ra_v, &rb_v),
2191                            (LuaValue::Int(_) | LuaValue::Float(_),
2192                             LuaValue::Int(_) | LuaValue::Float(_))) {
2193                            le_num(&ra_v, &rb_v)
2194                        } else {
2195                            state.set_ci_savedpc(ci, pc);
2196                            state.set_top(state.ci_top(ci));
2197                            let r = less_equal_others(state, &ra_v, &rb_v)?;
2198                            trap = state.ci_trap(ci);
2199                            r
2200                        };
2201                        if (cond as i32) != i.arg_k() {
2202                            pc += 1;
2203                        } else {
2204                            let next = state.proto_code(&cl, pc);
2205                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2206                            trap = state.ci_trap(ci);
2207                        }
2208                    }
2209                    // ── OP_EQK ────────────────────────────────────────────────
2210                    OpCode::EqK => {
2211                        let ra_v = state.get_at(base + i.arg_a());
2212                        let rb_v = state.proto_const(&cl, i.arg_b() as usize).clone();
2213                        let cond = equal_obj(None, &ra_v, &rb_v)? as u32;
2214                        if (cond as i32) != i.arg_k() {
2215                            pc += 1;
2216                        } else {
2217                            let next = state.proto_code(&cl, pc);
2218                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2219                            trap = state.ci_trap(ci);
2220                        }
2221                    }
2222                    // ── OP_EQI ────────────────────────────────────────────────
2223                    //    if (ttisinteger) cond = ivalue == im
2224                    //    elif (ttisfloat) cond = numeq(fltvalue, cast_num(im))
2225                    //    else cond = 0
2226                    OpCode::EqI => {
2227                        let ra_v = state.get_at(base + i.arg_a());
2228                        let im = i.arg_s_b() as i64;
2229                        let cond: bool = match &ra_v {
2230                            LuaValue::Int(iv) => *iv == im,
2231                            LuaValue::Float(fv) => *fv == im as f64,
2232                            _ => false,
2233                        };
2234                        if (cond as i32) != i.arg_k() {
2235                            pc += 1;
2236                        } else {
2237                            let next = state.proto_code(&cl, pc);
2238                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2239                            trap = state.ci_trap(ci);
2240                        }
2241                    }
2242                    // ── OP_LTI / OP_LEI / OP_GTI / OP_GEI ───────────────────
2243                    //              inv=0/0/1/1, tm=TM_LT/TM_LE/TM_LT/TM_LE)
2244                    OpCode::LtI => {
2245                        let ra = base + i.arg_a();
2246                        let im = i.arg_s_b() as i64;
2247                        let fast_cond = match &state.stack[ra.0 as usize].val {
2248                            LuaValue::Int(ia) => Some(*ia < im),
2249                            LuaValue::Float(fa) => Some(*fa < im as f64),
2250                            _ => None,
2251                        };
2252                        let cond = match fast_cond {
2253                            Some(cond) => cond,
2254                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, false, TagMethod::Lt)?,
2255                        };
2256                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2257                    }
2258                    OpCode::LeI => {
2259                        let ra = base + i.arg_a();
2260                        let im = i.arg_s_b() as i64;
2261                        let fast_cond = match &state.stack[ra.0 as usize].val {
2262                            LuaValue::Int(ia) => Some(*ia <= im),
2263                            LuaValue::Float(fa) => Some(*fa <= im as f64),
2264                            _ => None,
2265                        };
2266                        let cond = match fast_cond {
2267                            Some(cond) => cond,
2268                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, false, TagMethod::Le)?,
2269                        };
2270                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2271                    }
2272                    OpCode::GtI => {
2273                        let ra = base + i.arg_a();
2274                        let im = i.arg_s_b() as i64;
2275                        let fast_cond = match &state.stack[ra.0 as usize].val {
2276                            LuaValue::Int(ia) => Some(*ia > im),
2277                            LuaValue::Float(fa) => Some(*fa > im as f64),
2278                            _ => None,
2279                        };
2280                        let cond = match fast_cond {
2281                            Some(cond) => cond,
2282                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, true, TagMethod::Lt)?,
2283                        };
2284                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2285                    }
2286                    OpCode::GeI => {
2287                        let ra = base + i.arg_a();
2288                        let im = i.arg_s_b() as i64;
2289                        let fast_cond = match &state.stack[ra.0 as usize].val {
2290                            LuaValue::Int(ia) => Some(*ia >= im),
2291                            LuaValue::Float(fa) => Some(*fa >= im as f64),
2292                            _ => None,
2293                        };
2294                        let cond = match fast_cond {
2295                            Some(cond) => cond,
2296                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, true, TagMethod::Le)?,
2297                        };
2298                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2299                    }
2300                    // ── OP_TEST ────────────────────────────────────────────────
2301                    OpCode::Test => {
2302                        let ra_v = state.get_at(base + i.arg_a());
2303                        let cond = !matches!(ra_v, LuaValue::Nil | LuaValue::Bool(false));
2304                        if (cond as i32) != i.arg_k() {
2305                            pc += 1;
2306                        } else {
2307                            let next = state.proto_code(&cl, pc);
2308                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2309                            trap = state.ci_trap(ci);
2310                        }
2311                    }
2312                    // ── OP_TESTSET ─────────────────────────────────────────────
2313                    //    else { setobj2s(L, ra, rb); donextjump(ci); }
2314                    OpCode::TestSet => {
2315                        let ra = base + i.arg_a();
2316                        let rb_v = state.get_at(base + i.arg_b());
2317                        let falsy = matches!(rb_v, LuaValue::Nil | LuaValue::Bool(false));
2318                        if (falsy as i32) == i.arg_k() {
2319                            pc += 1;
2320                        } else {
2321                            state.set_at(ra, rb_v);
2322                            let next = state.proto_code(&cl, pc);
2323                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2324                            trap = state.ci_trap(ci);
2325                        }
2326                    }
2327                    // ── OP_CALL ────────────────────────────────────────────────
2328                    //      updatetrap(ci);
2329                    //    else { ci = newci; goto startfunc; }
2330                    OpCode::Call => {
2331                        let ra = base + i.arg_a();
2332                        let b = i.arg_b();
2333                        let nresults = i.arg_c() as i32 - 1;
2334                        if b != 0 {
2335                            state.set_top(ra + b);
2336                        }
2337                        state.set_ci_savedpc(ci, pc); // savepc
2338                        let had_hook = state.hookmask != 0;
2339                        match state.precall(ra, nresults)? {
2340                            None => {
2341                                // C functions such as debug.sethook can change
2342                                // hook state during the call, so refresh the VM
2343                                // trap when hooks were or became relevant.
2344                                if had_hook || state.hookmask != 0 {
2345                                    trap = state.ci_trap(ci); // updatetrap
2346                                }
2347                            }
2348                            Some(new_ci) => {
2349                                // Lua function — goto startfunc
2350                                ci = new_ci;
2351                                continue 'startfunc;
2352                            }
2353                        }
2354                    }
2355                    // ── OP_TAILCALL ────────────────────────────────────────────
2356                    //      goto startfunc;
2357                    //    else { ci->func.p -= delta; luaD_poscall(L, ci, n);
2358                    //            updatetrap; goto ret; }
2359                    OpCode::TailCall => {
2360                        let ra = base + i.arg_a();
2361                        let b = i.arg_b();
2362                        let nparams1 = i.arg_c();
2363                        let delta = if nparams1 != 0 {
2364                            state.ci_nextraargs(ci) + nparams1 as i32
2365                        } else {
2366                            0
2367                        };
2368                        let top_b: i32 = if b != 0 {
2369                            state.set_top(ra + b);
2370                            b
2371                        } else {
2372                            state.top_idx() - ra
2373                        };
2374                        state.set_ci_savedpc(ci, pc);
2375                        if i.test_k() {
2376                            state.close_upvals_from_base(ci)?;
2377                        }
2378                        let n = state.pretailcall(ci, ra, top_b, delta)?;
2379                        if n < 0 {
2380                            // Lua function — goto startfunc
2381                            continue 'startfunc;
2382                        } else {
2383                            // C function — ci->func.p -= delta; luaD_poscall; goto ret
2384                            state.ci_adjust_func(ci, delta);
2385                            state.poscall(ci, n as u32)?;
2386                            if state.hookmask != 0 {
2387                                trap = state.ci_trap(ci);
2388                            }
2389                            break 'dispatch; // goto ret
2390                        }
2391                    }
2392                    // ── OP_RETURN ──────────────────────────────────────────────
2393                    //    savepc; if TESTARG_k: close upvals;
2394                    //    if nparams1: ci->func -= nextraargs+nparams1;
2395                    //    L->top.p = ra+n; luaD_poscall; goto ret
2396                    OpCode::Return => {
2397                        let ra = base + i.arg_a();
2398                        let n_raw = i.arg_b() as i32 - 1;
2399                        let nparams1 = i.arg_c();
2400                        let n: u32 = if n_raw < 0 {
2401                            (state.top_idx() - ra) as u32
2402                        } else {
2403                            n_raw as u32
2404                        };
2405                        state.set_ci_savedpc(ci, pc);
2406                        if i.test_k() {
2407                            state.ci_nres_set(ci, n as i32);
2408                            let ci_top = state.ci_top(ci);
2409                            if state.top_idx().0 < ci_top.0 {
2410                                state.set_top(ci_top);
2411                            }
2412                            crate::func::close(state, base, crate::func::CLOSE_K_TOP, true)?;
2413                            if state.hookmask != 0 {
2414                                trap = state.ci_trap(ci);
2415                            }
2416                            base = state.ci_base(ci); // updatestack
2417                        }
2418                        if nparams1 != 0 {
2419                            let nextraargs = state.ci_nextraargs(ci) as u32;
2420                            state.ci_adjust_func(ci, nextraargs as i32 + nparams1 as i32);
2421                        }
2422                        state.set_top(ra + n as i32);
2423                        state.poscall(ci, n)?;
2424                        if state.hookmask != 0 {
2425                            trap = state.ci_trap(ci);
2426                        }
2427                        break 'dispatch; // goto ret
2428                    }
2429                    // ── OP_RETURN0 ─────────────────────────────────────────────
2430                    //    else { L->ci = ci->previous; L->top = base-1;
2431                    //           for (nres = ci->nresults; nres > 0; nres--)
2432                    //             setnilvalue(L->top++) }
2433                    //    goto ret;
2434                    OpCode::Return0 => {
2435                        if state.hookmask == 0 {
2436                            let ci_slot = ci.as_usize();
2437                            let nres = state.call_info[ci_slot].nresults as i32;
2438                            state.ci = state.call_info[ci_slot]
2439                                .previous
2440                                .expect("RETURN0: returning frame has no previous CallInfo");
2441                            state.top = base - 1;
2442                            for _ in 0..nres.max(0) {
2443                                state.push(LuaValue::Nil);
2444                            }
2445                        } else {
2446                            return0_hook(state, ci, base, i, pc, &mut trap)?;
2447                        }
2448                        break 'dispatch; // goto ret
2449                    }
2450                    // ── OP_RETURN1 ─────────────────────────────────────────────
2451                    //    else { nres = ci->nresults; ci = ci->previous; ...handle results... }
2452                    //    goto ret;
2453                    OpCode::Return1 => {
2454                        if state.hookmask == 0 {
2455                            let ci_slot = ci.as_usize();
2456                            let nres = state.call_info[ci_slot].nresults as i32;
2457                            state.ci = state.call_info[ci_slot]
2458                                .previous
2459                                .expect("RETURN1: returning frame has no previous CallInfo");
2460                            if nres == 0 {
2461                                state.top = base - 1;
2462                            } else {
2463                                let ra = base + i.arg_a();
2464                                state.stack[(base - 1).0 as usize].val =
2465                                    state.stack[ra.0 as usize].val; // at least this result
2466                                state.top = base;
2467                                for _ in 1..nres.max(0) {
2468                                    state.push(LuaValue::Nil);
2469                                }
2470                            }
2471                        } else {
2472                            return1_hook(state, ci, base, i, pc, &mut trap)?;
2473                        }
2474                        break 'dispatch; // goto ret
2475                    }
2476                    // ── OP_FORLOOP ─────────────────────────────────────────────
2477                    //    else if (floatforloop(ra)) pc -= GETARG_Bx(i)
2478                    //    updatetrap(ci);
2479                    OpCode::ForLoop => {
2480                        let ra = base + i.arg_a();
2481                        let ra_u = ra.0 as usize;
2482                        if let LuaValue::Int(step) = state.stack[ra_u + 2].val {
2483                            let count = match state.stack[ra_u + 1].val {
2484                                LuaValue::Int(c) => c as u64,
2485                                _ => 0,
2486                            };
2487                            if count > 0 {
2488                                let idx = match state.stack[ra_u].val {
2489                                    LuaValue::Int(x) => x,
2490                                    _ => 0,
2491                                };
2492                                state.stack[ra_u + 1].val = LuaValue::Int((count - 1) as i64);
2493                                let new_idx = intop_add(idx, step);
2494                                state.stack[ra_u].val = LuaValue::Int(new_idx);
2495                                state.stack[ra_u + 3].val = LuaValue::Int(new_idx);
2496                                pc = (pc as i64 - i.arg_bx() as i64) as u32;
2497                            }
2498                        } else if float_for_loop(state, ra) {
2499                            pc = (pc as i64 - i.arg_bx() as i64) as u32;
2500                        }
2501                        trap = state.ci_trap(ci);
2502                    }
2503                    // ── OP_FORPREP ─────────────────────────────────────────────
2504                    OpCode::ForPrep => {
2505                        let ra = base + i.arg_a();
2506                        state.set_ci_savedpc(ci, pc);
2507                        state.set_top(state.ci_top(ci));
2508                        if forprep(state, ra)? {
2509                            pc = (pc as i64 + i.arg_bx() as i64 + 1) as u32;
2510                        }
2511                    }
2512                    // ── OP_TFORPREP ────────────────────────────────────────────
2513                    //    pc += GETARG_Bx(i); i = *pc++; assert(OP_TFORCALL && ra==RA(i));
2514                    //    goto l_tforcall;
2515                    OpCode::TForPrep => {
2516                        let ra = base + i.arg_a();
2517                        state.set_ci_savedpc(ci, pc);
2518                        state.set_top(state.ci_top(ci));
2519                        state.new_tbc_upval(ra + 3)?;
2520                        pc = (pc as i64 + i.arg_bx() as i64) as u32;
2521                        let tfc_i = state.proto_code(&cl, pc);
2522                        pc += 1;
2523                        debug_assert!(tfc_i.opcode() == OpCode::TForCall);
2524                        // inline l_tforcall:
2525                        let tfc_ra = base + tfc_i.arg_a();
2526                        for k in 0..3u32 {
2527                            let v = state.get_at(tfc_ra + k as i32);
2528                            state.set_at(tfc_ra + 4 + k as i32, v);
2529                        }
2530                        state.set_top(tfc_ra + 4 + 3);
2531                        state.set_ci_savedpc(ci, pc);
2532                        state.call_at(tfc_ra + 4, tfc_i.arg_c() as i32)?;
2533                        trap = state.ci_trap(ci);
2534                        base = state.ci_base(ci); // updatestack
2535                        let tfl_i = state.proto_code(&cl, pc);
2536                        pc += 1;
2537                        debug_assert!(tfl_i.opcode() == OpCode::TForLoop);
2538                        let tfl_ra = base + tfl_i.arg_a();
2539                        // inline l_tforloop:
2540                        if !matches!(state.get_at(tfl_ra + 4), LuaValue::Nil) {
2541                            let v = state.get_at(tfl_ra + 4);
2542                            state.set_at(tfl_ra + 2, v);
2543                            pc = (pc as i64 - tfl_i.arg_bx() as i64) as u32;
2544                        }
2545                    }
2546                    // ── OP_TFORCALL ────────────────────────────────────────────
2547                    OpCode::TForCall => {
2548                        let ra = base + i.arg_a();
2549                        for k in 0..3u32 {
2550                            let v = state.get_at(ra + k as i32);
2551                            state.set_at(ra + 4 + k as i32, v);
2552                        }
2553                        state.set_top(ra + 4 + 3);
2554                        state.set_ci_savedpc(ci, pc);
2555                        state.call_at(ra + 4, i.arg_c() as i32)?;
2556                        trap = state.ci_trap(ci);
2557                        base = state.ci_base(ci); // updatestack
2558                        let tfl_i = state.proto_code(&cl, pc);
2559                        pc += 1;
2560                        debug_assert!(tfl_i.opcode() == OpCode::TForLoop);
2561                        let tfl_ra = base + tfl_i.arg_a();
2562                        if !matches!(state.get_at(tfl_ra + 4), LuaValue::Nil) {
2563                            let v = state.get_at(tfl_ra + 4);
2564                            state.set_at(tfl_ra + 2, v);
2565                            pc = (pc as i64 - tfl_i.arg_bx() as i64) as u32;
2566                        }
2567                    }
2568                    // ── OP_TFORLOOP ────────────────────────────────────────────
2569                    OpCode::TForLoop => {
2570                        let ra = base + i.arg_a();
2571                        if !matches!(state.get_at(ra + 4), LuaValue::Nil) {
2572                            let v = state.get_at(ra + 4);
2573                            state.set_at(ra + 2, v);
2574                            pc = (pc as i64 - i.arg_bx() as i64) as u32;
2575                        }
2576                    }
2577                    // ── OP_SETLIST ─────────────────────────────────────────────
2578                    //    if TESTARG_k: last += Ax * (MAXARG_C+1); pc++;
2579                    //    for (; n > 0; n--) h->array[last-1] = val; luaC_barrierback
2580                    OpCode::SetList => {
2581                        let ra = base + i.arg_a();
2582                        let n_raw = i.arg_b();
2583                        let mut last = i.arg_c();
2584                        let t_val = state.get_at(ra);
2585                        let n: i32 = if n_raw == 0 {
2586                            state.top_idx() - ra - 1
2587                        } else {
2588                            state.set_top(state.ci_top(ci));
2589                            n_raw
2590                        };
2591                        last += n;
2592                        if i.test_k() {
2593                            let extra = state.proto_code(&cl, pc);
2594                            pc += 1;
2595                            const MAXARG_C: i32 = (1 << 8) - 1;
2596                            last += extra.arg_ax() * (MAXARG_C + 1);
2597                        }
2598                        state.table_ensure_array(&t_val, last as usize)?;
2599                        for k in (1..=n).rev() {
2600                            let val = state.get_at(ra + k as i32);
2601                            state.table_array_set(&t_val, (last - 1) as usize, val.clone())?;
2602                            last -= 1;
2603                            state.gc_barrier_back(&t_val, &val);
2604                        }
2605                    }
2606                    // ── OP_CLOSURE ─────────────────────────────────────────────
2607                    //    halfProtect(pushclosure(L, p, cl->upvals, base, ra));
2608                    //    checkGC(L, ra+1);
2609                    OpCode::Closure => {
2610                        let ra = base + i.arg_a();
2611                        let proto_idx = i.arg_bx() as usize;
2612                        state.set_ci_savedpc(ci, pc);
2613                        state.set_top(state.ci_top(ci));
2614                        push_closure(state, proto_idx, ci, base, ra)?;
2615                        // checkGC
2616                        state.set_ci_savedpc(ci, pc);
2617                        state.set_top(ra + 1);
2618                        state.gc_cond_step();
2619                        trap = state.ci_trap(ci);
2620                    }
2621                    // ── OP_VARARG ──────────────────────────────────────────────
2622                    OpCode::VarArg => {
2623                        let ra = base + i.arg_a();
2624                        let n = i.arg_c() as i32 - 1;
2625                        state.set_ci_savedpc(ci, pc);
2626                        state.set_top(state.ci_top(ci));
2627                        state.get_varargs(ci, ra, n)?;
2628                        trap = state.ci_trap(ci);
2629                    }
2630                    // ── OP_VARARGPREP ──────────────────────────────────────────
2631                    //    if (trap) luaD_hookcall(L, ci); L->oldpc = 1;
2632                    //    updatebase(ci);
2633                    OpCode::VarArgPrep => {
2634                        let nparams = i.arg_a();
2635                        state.set_ci_savedpc(ci, pc);
2636                        state.adjust_varargs(ci, nparams, &cl)?;
2637                        trap = state.ci_trap(ci);
2638                        if trap {
2639                            state.hook_call(ci)?;
2640                            state.set_oldpc(1);
2641                        }
2642                        base = state.ci_base(ci);
2643                    }
2644                    // ── OP_EXTRAARG ────────────────────────────────────────────
2645                    OpCode::ExtraArg => {
2646                        debug_assert!(false, "OP_EXTRAARG executed directly");
2647                    }
2648                } // end match opcode
2649            } // end 'dispatch loop
2650
2651            // ── ret: label ──────────────────────────────────────────────────
2652            if state.ci_is_fresh(ci) {
2653                return Ok(());
2654            } else {
2655                ci = state.ci_previous(ci).expect("ci_previous: not fresh frame must have previous");
2656                continue 'returning;
2657            }
2658        } // end 'returning loop
2659    } // end 'startfunc loop
2660}
2661
2662// ─── Local opcode dispatch helpers ───────────────────────────────────────────
2663
2664#[inline(always)]
2665fn number_value(v: LuaValue) -> Option<f64> {
2666    match v {
2667        LuaValue::Float(f) => Some(f),
2668        LuaValue::Int(i) => Some(i as f64),
2669        _ => None,
2670    }
2671}
2672
2673/// Increments `pc` on success (the `pc++` in the C macros).
2674#[allow(dead_code)]
2675#[inline]
2676fn arith_op_aux_rr(
2677    state: &mut LuaState,
2678    ra: StackIdx,
2679    v1: &LuaValue,
2680    v2: &LuaValue,
2681    pc: &mut u32,
2682    iop: fn(i64, i64) -> i64,
2683    fop: fn(f64, f64) -> f64,
2684) {
2685    if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (v1, v2) {
2686        *pc += 1;
2687        state.set_at(ra, LuaValue::Int(iop(*i1, *i2)));
2688    } else {
2689        arith_float_aux(state, ra, v1, v2, pc, fop);
2690    }
2691}
2692
2693#[allow(dead_code)]
2694#[inline]
2695fn arith_float_aux(
2696    state: &mut LuaState,
2697    ra: StackIdx,
2698    v1: &LuaValue,
2699    v2: &LuaValue,
2700    pc: &mut u32,
2701    fop: fn(f64, f64) -> f64,
2702) {
2703    let n1 = match v1 {
2704        LuaValue::Float(f) => Some(*f),
2705        LuaValue::Int(i) => Some(*i as f64),
2706        _ => None,
2707    };
2708    let n2 = match v2 {
2709        LuaValue::Float(f) => Some(*f),
2710        LuaValue::Int(i) => Some(*i as f64),
2711        _ => None,
2712    };
2713    if let (Some(n1), Some(n2)) = (n1, n2) {
2714        *pc += 1;
2715        state.set_at(ra, LuaValue::Float(fop(n1, n2)));
2716    }
2717}
2718
2719#[allow(dead_code)]
2720#[inline]
2721fn arith_op_checked(
2722    state: &mut LuaState,
2723    ra: StackIdx,
2724    v1: &LuaValue,
2725    v2: &LuaValue,
2726    pc: &mut u32,
2727    iop: fn(i64, i64) -> Result<i64, LuaError>,
2728    fop: fn(f64, f64) -> f64,
2729) -> Result<(), LuaError> {
2730    if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (v1, v2) {
2731        *pc += 1;
2732        let result = iop(*i1, *i2).map_err(|e| match e {
2733            LuaError::Runtime(LuaValue::Str(s)) => {
2734                crate::debug::prefixed_runtime_pub(state, s.as_bytes().to_vec())
2735            }
2736            other => other,
2737        })?;
2738        state.set_at(ra, LuaValue::Int(result));
2739    } else {
2740        arith_float_aux(state, ra, v1, v2, pc, fop);
2741    }
2742    Ok(())
2743}
2744
2745#[allow(dead_code)]
2746#[inline]
2747fn bitwise_op_k(
2748    state: &mut LuaState,
2749    ra: StackIdx,
2750    v1: &LuaValue,
2751    v2: &LuaValue, // must be integer (K constant)
2752    pc: &mut u32,
2753    op: fn(i64, i64) -> i64,
2754) {
2755    let i2 = match v2 {
2756        LuaValue::Int(i) => *i,
2757        _ => return,
2758    };
2759    if let Some(i1) = to_integer_ns(v1, F2Imod::Eq) {
2760        *pc += 1;
2761        state.set_at(ra, LuaValue::Int(op(i1, i2)));
2762    }
2763}
2764
2765#[allow(dead_code)]
2766#[inline]
2767fn bitwise_op_rr(
2768    state: &mut LuaState,
2769    ra: StackIdx,
2770    v1: &LuaValue,
2771    v2: &LuaValue,
2772    pc: &mut u32,
2773    op: fn(i64, i64) -> i64,
2774) {
2775    if let (Some(i1), Some(i2)) = (
2776        to_integer_ns(v1, F2Imod::Eq),
2777        to_integer_ns(v2, F2Imod::Eq),
2778    ) {
2779        *pc += 1;
2780        state.set_at(ra, LuaValue::Int(op(i1, i2)));
2781    }
2782}
2783
2784/// `right = true` negates `y` for right-shift semantics.
2785#[allow(dead_code)]
2786#[inline]
2787fn bitwise_shift_rr(
2788    state: &mut LuaState,
2789    ra: StackIdx,
2790    v1: &LuaValue,
2791    v2: &LuaValue,
2792    pc: &mut u32,
2793    right: bool,
2794) {
2795    if let (Some(i1), Some(i2)) = (
2796        to_integer_ns(v1, F2Imod::Eq),
2797        to_integer_ns(v2, F2Imod::Eq),
2798    ) {
2799        let y = if right { intop_sub(0, i2) } else { i2 };
2800        *pc += 1;
2801        state.set_at(ra, LuaValue::Int(shiftl(i1, y)));
2802    }
2803}
2804
2805/// Cold half of C's `op_orderI` macro: only reached when the operand is not a
2806/// plain integer/float and a metamethod lookup may be needed.
2807#[cold]
2808#[inline(never)]
2809#[allow(clippy::too_many_arguments)]
2810fn order_imm_slow(
2811    state: &mut LuaState,
2812    ra: StackIdx,
2813    pc: u32,
2814    trap: &mut bool,
2815    ci: CallInfoIdx,
2816    i: Instruction,
2817    im: i64,
2818    inv: bool,
2819    tm: TagMethod,
2820) -> Result<bool, LuaError> {
2821    let ra_v = state.get_at(ra);
2822    let isf = i.arg_c() != 0;
2823    state.set_ci_savedpc(ci, pc);
2824    state.set_top(state.ci_top(ci));
2825    let r = state.call_order_i_tm(&ra_v, im, inv, isf, tm)?;
2826    *trap = state.ci_trap(ci);
2827    Ok(r)
2828}
2829
2830#[inline(always)]
2831fn finish_order_imm_jump(
2832    state: &mut LuaState,
2833    cl: &lua_types::GcRef<lua_types::LuaLClosure>,
2834    pc: &mut u32,
2835    trap: &mut bool,
2836    ci: CallInfoIdx,
2837    i: Instruction,
2838    cond: bool,
2839) {
2840    if (cond as i32) != i.arg_k() {
2841        *pc += 1;
2842    } else {
2843        let next = state.proto_code(&cl, *pc);
2844        *pc = (*pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2845        *trap = state.ci_trap(ci);
2846    }
2847}
2848
2849#[cold]
2850#[inline(never)]
2851fn return0_hook(
2852    state: &mut LuaState,
2853    ci: CallInfoIdx,
2854    base: StackIdx,
2855    i: Instruction,
2856    pc: u32,
2857    trap: &mut bool,
2858) -> Result<(), LuaError> {
2859    let ra = base + i.arg_a();
2860    state.set_top(ra);
2861    state.set_ci_savedpc(ci, pc);
2862    state.poscall(ci, 0)?;
2863    *trap = true;
2864    Ok(())
2865}
2866
2867#[cold]
2868#[inline(never)]
2869fn return1_hook(
2870    state: &mut LuaState,
2871    ci: CallInfoIdx,
2872    base: StackIdx,
2873    i: Instruction,
2874    pc: u32,
2875    trap: &mut bool,
2876) -> Result<(), LuaError> {
2877    let ra = base + i.arg_a();
2878    state.set_top(ra + 1);
2879    state.set_ci_savedpc(ci, pc);
2880    state.poscall(ci, 1)?;
2881    *trap = true;
2882    Ok(())
2883}
2884
2885// ──────────────────────────────────────────────────────────────────────────
2886// PORT STATUS
2887//   source:        src/lvm.c  (1899 lines, 32 functions)
2888//   target_crate:  lua-vm
2889//   confidence:    medium
2890//   todos:         6
2891//   port_notes:    4
2892//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
2893//   notes:         All opcode handlers and helpers translated; LuaState methods
2894//                  referenced (fast_get, precall, poscall, etc.) are stubs that
2895//                  Phase B will land.  The execute() goto flow is modelled with
2896//                  labelled Rust loops ('startfunc/'returning/'dispatch).
2897//                  str_to_number is a stub pending luaO_str2num port (TODO #1).
2898//                  strcoll replaced with byte-lexicographic order (TODO #2).
2899//                  order_imm_op uses LuaValue as a stand-in for GcRef<LuaClosure>
2900//                  (TODO #3).  ClosureRef type alias not yet defined (TODO #4-6).
2901// ──────────────────────────────────────────────────────────────────────────