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.get_at(rb);
1465                        state.set_at(ra, 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 = state.new_table();
1741                        state.set_at(ra, LuaValue::Table(t.clone()));
1742                        if b != 0 || c != 0 {
1743                            state.table_resize(&t, c as usize, b as usize)?;
1744                        }
1745                        state.set_ci_savedpc(ci, pc);
1746                        state.set_top(ra + 1);
1747                        state.gc_cond_step();
1748                        trap = state.ci_trap(ci);
1749                    }
1750                    // ── OP_SELF ───────────────────────────────────────────────
1751                    OpCode::Self_ => {
1752                        let ra = base + i.arg_a();
1753                        let rb_idx = base + i.arg_b();
1754                        let rb_v = state.get_at(rb_idx);
1755                        let k_idx = i.arg_c() as usize; // RKC key (always a string)
1756                        let key = if i.test_k() {
1757                            state.proto_const(&cl, k_idx).clone()
1758                        } else {
1759                            state.get_at(base + i.arg_c())
1760                        };
1761                        state.set_at(ra + 1, rb_v.clone());
1762                        match state.fast_get_short_str(&rb_v, &key)? {
1763                            Some(v) => state.set_at(ra, v),
1764                            None => {
1765                                state.set_ci_savedpc(ci, pc);
1766                                state.set_top(state.ci_top(ci));
1767                                finish_get(state, rb_v, key, ra, true, Some(rb_idx))?;
1768                                trap = state.ci_trap(ci);
1769                            }
1770                        }
1771                    }
1772                    // ── Arithmetic immediates ──────────────────────────────────
1773                    OpCode::AddI => {
1774                        let ra = base + i.arg_a();
1775                        let rb = base + i.arg_b();
1776                        let imm = i.arg_s_c() as i64;
1777                        if let Some(iv1) = state.get_int_at(rb) {
1778                            pc += 1;
1779                            state.set_at(ra, LuaValue::Int(intop_add(iv1, imm)));
1780                        } else if let Some(nb) = state.get_float_at(rb) {
1781                            pc += 1;
1782                            state.set_at(ra, LuaValue::Float(nb + imm as f64));
1783                        }
1784                    }
1785                    // ── Arithmetic with K constant operand ─────────────────────
1786                    OpCode::AddK => {
1787                        let ra = base + i.arg_a();
1788                        let rb = base + i.arg_b();
1789                        let kidx = i.arg_c() as usize;
1790                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
1791                            pc += 1;
1792                            state.set_at(ra, LuaValue::Int(intop_add(i1, i2)));
1793                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1794                            pc += 1;
1795                            state.set_at(ra, LuaValue::Float(n1 + n2));
1796                        }
1797                    }
1798                    OpCode::SubK => {
1799                        let ra = base + i.arg_a();
1800                        let rb = base + i.arg_b();
1801                        let kidx = i.arg_c() as usize;
1802                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
1803                            pc += 1;
1804                            state.set_at(ra, LuaValue::Int(intop_sub(i1, i2)));
1805                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1806                            pc += 1;
1807                            state.set_at(ra, LuaValue::Float(n1 - n2));
1808                        }
1809                    }
1810                    OpCode::MulK => {
1811                        let ra = base + i.arg_a();
1812                        let rb = base + i.arg_b();
1813                        let kidx = i.arg_c() as usize;
1814                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
1815                            pc += 1;
1816                            state.set_at(ra, LuaValue::Int(intop_mul(i1, i2)));
1817                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1818                            pc += 1;
1819                            state.set_at(ra, LuaValue::Float(n1 * n2));
1820                        }
1821                    }
1822                    OpCode::ModK => {
1823                        let ra = base + i.arg_a();
1824                        let v1 = state.get_at(base + i.arg_b());
1825                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1826                        state.set_ci_savedpc(ci, pc); // savestate for div-by-zero
1827                        state.set_top(state.ci_top(ci));
1828                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
1829                            |a, b| imod(a, b), fmodf)?;
1830                    }
1831                    OpCode::PowK => {
1832                        let ra = base + i.arg_a();
1833                        let rb = base + i.arg_b();
1834                        let kidx = i.arg_c() as usize;
1835                        if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1836                            pc += 1;
1837                            let r = if n2 == 2.0 { n1 * n1 } else { n1.powf(n2) };
1838                            state.set_at(ra, LuaValue::Float(r));
1839                        }
1840                    }
1841                    OpCode::DivK => {
1842                        let ra = base + i.arg_a();
1843                        let rb = base + i.arg_b();
1844                        let kidx = i.arg_c() as usize;
1845                        if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1846                            pc += 1;
1847                            state.set_at(ra, LuaValue::Float(n1 / n2));
1848                        }
1849                    }
1850                    OpCode::IDivK => {
1851                        let ra = base + i.arg_a();
1852                        let v1 = state.get_at(base + i.arg_b());
1853                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1854                        state.set_ci_savedpc(ci, pc);
1855                        state.set_top(state.ci_top(ci));
1856                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
1857                            |a, b| idiv(a, b), |a, b| (a / b).floor())?;
1858                    }
1859                    OpCode::BAndK => {
1860                        let ra = base + i.arg_a();
1861                        let v1 = state.get_at(base + i.arg_b());
1862                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1863                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_band);
1864                    }
1865                    OpCode::BOrK => {
1866                        let ra = base + i.arg_a();
1867                        let v1 = state.get_at(base + i.arg_b());
1868                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1869                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_bor);
1870                    }
1871                    OpCode::BXOrK => {
1872                        let ra = base + i.arg_a();
1873                        let v1 = state.get_at(base + i.arg_b());
1874                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1875                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_bxor);
1876                    }
1877                    OpCode::ShrI => {
1878                        let ra = base + i.arg_a();
1879                        let v = state.get_at(base + i.arg_b());
1880                        let ic = i.arg_s_c() as i64;
1881                        if let Some(ib) = to_integer_ns(&v, F2Imod::Eq) {
1882                            pc += 1;
1883                            state.set_at(ra, LuaValue::Int(shiftl(ib, -ic)));
1884                        }
1885                    }
1886                    OpCode::ShlI => {
1887                        let ra = base + i.arg_a();
1888                        let v = state.get_at(base + i.arg_b());
1889                        let ic = i.arg_s_c() as i64;
1890                        if let Some(ib) = to_integer_ns(&v, F2Imod::Eq) {
1891                            pc += 1;
1892                            state.set_at(ra, LuaValue::Int(shiftl(ic, ib)));
1893                        }
1894                    }
1895                    // ── Arithmetic with register operands ──────────────────────
1896                    OpCode::Add => {
1897                        let ra = base + i.arg_a();
1898                        let rb = base + i.arg_b();
1899                        let rc = base + i.arg_c();
1900                        let ra_u = ra.0 as usize;
1901                        let rb_v = state.stack[rb.0 as usize].val;
1902                        let rc_v = state.stack[rc.0 as usize].val;
1903                        if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (rb_v, rc_v) {
1904                            pc += 1;
1905                            state.stack[ra_u].val = LuaValue::Int(intop_add(i1, i2));
1906                        } else if let (Some(n1), Some(n2)) = (number_value(rb_v), number_value(rc_v)) {
1907                            pc += 1;
1908                            state.stack[ra_u].val = LuaValue::Float(n1 + n2);
1909                        }
1910                    }
1911                    OpCode::Sub => {
1912                        let ra = base + i.arg_a();
1913                        let rb = base + i.arg_b();
1914                        let rc = base + i.arg_c();
1915                        let ra_u = ra.0 as usize;
1916                        let rb_v = state.stack[rb.0 as usize].val;
1917                        let rc_v = state.stack[rc.0 as usize].val;
1918                        if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (rb_v, rc_v) {
1919                            pc += 1;
1920                            state.stack[ra_u].val = LuaValue::Int(intop_sub(i1, i2));
1921                        } else if let (Some(n1), Some(n2)) = (number_value(rb_v), number_value(rc_v)) {
1922                            pc += 1;
1923                            state.stack[ra_u].val = LuaValue::Float(n1 - n2);
1924                        }
1925                    }
1926                    OpCode::Mul => {
1927                        let ra = base + i.arg_a();
1928                        let rb = base + i.arg_b();
1929                        let rc = base + i.arg_c();
1930                        if let Some((i1, i2)) = state.get_int_pair_at(rb, rc) {
1931                            pc += 1;
1932                            state.set_at(ra, LuaValue::Int(intop_mul(i1, i2)));
1933                        } else if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
1934                            pc += 1;
1935                            state.set_at(ra, LuaValue::Float(n1 * n2));
1936                        }
1937                    }
1938                    OpCode::Mod => {
1939                        let ra = base + i.arg_a();
1940                        let v1 = state.get_at(base + i.arg_b());
1941                        let v2 = state.get_at(base + i.arg_c());
1942                        state.set_ci_savedpc(ci, pc);
1943                        state.set_top(state.ci_top(ci));
1944                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
1945                            |a, b| imod(a, b), fmodf)?;
1946                    }
1947                    OpCode::Pow => {
1948                        let ra = base + i.arg_a();
1949                        let rb = base + i.arg_b();
1950                        let rc = base + i.arg_c();
1951                        if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
1952                            pc += 1;
1953                            let r = if n2 == 2.0 { n1 * n1 } else { n1.powf(n2) };
1954                            state.set_at(ra, LuaValue::Float(r));
1955                        }
1956                    }
1957                    OpCode::Div => {
1958                        let ra = base + i.arg_a();
1959                        let rb = base + i.arg_b();
1960                        let rc = base + i.arg_c();
1961                        if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
1962                            pc += 1;
1963                            state.set_at(ra, LuaValue::Float(n1 / n2));
1964                        }
1965                    }
1966                    OpCode::IDiv => {
1967                        let ra = base + i.arg_a();
1968                        let v1 = state.get_at(base + i.arg_b());
1969                        let v2 = state.get_at(base + i.arg_c());
1970                        state.set_ci_savedpc(ci, pc);
1971                        state.set_top(state.ci_top(ci));
1972                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
1973                            |a, b| idiv(a, b), |a, b| (a / b).floor())?;
1974                    }
1975                    // ── Bitwise with register operands ─────────────────────────
1976                    // if (tointegerns(v1, &i1) && tointegerns(v2, &i2)) { pc++; setivalue... }
1977                    OpCode::BAnd => {
1978                        let ra = base + i.arg_a();
1979                        let v1 = state.get_at(base + i.arg_b());
1980                        let v2 = state.get_at(base + i.arg_c());
1981                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_band);
1982                    }
1983                    OpCode::BOr => {
1984                        let ra = base + i.arg_a();
1985                        let v1 = state.get_at(base + i.arg_b());
1986                        let v2 = state.get_at(base + i.arg_c());
1987                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_bor);
1988                    }
1989                    OpCode::BXOr => {
1990                        let ra = base + i.arg_a();
1991                        let v1 = state.get_at(base + i.arg_b());
1992                        let v2 = state.get_at(base + i.arg_c());
1993                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_bxor);
1994                    }
1995                    OpCode::Shr => {
1996                        let ra = base + i.arg_a();
1997                        let v1 = state.get_at(base + i.arg_b());
1998                        let v2 = state.get_at(base + i.arg_c());
1999                        bitwise_shift_rr(state, ra, &v1, &v2, &mut pc, true);
2000                    }
2001                    OpCode::Shl => {
2002                        let ra = base + i.arg_a();
2003                        let v1 = state.get_at(base + i.arg_b());
2004                        let v2 = state.get_at(base + i.arg_c());
2005                        bitwise_shift_rr(state, ra, &v1, &v2, &mut pc, false);
2006                    }
2007                    // ── OP_MMBIN ─────────────────────────────────────────────
2008                    // Instruction pi = *(pc - 2); TMS tm = (TMS)GETARG_C(i);
2009                    // StkId result = RA(pi);
2010                    // Protect(luaT_trybinTM(L, s2v(ra), rb, result, tm));
2011                    OpCode::MmBin => {
2012                        let ra_idx = base + i.arg_a();
2013                        let rb_idx = base + i.arg_b();
2014                        let ra_v = state.get_at(ra_idx);
2015                        let rb_v = state.get_at(rb_idx);
2016                        let tm = tagmethod_from_index(i.arg_c() as usize);
2017                        let prev_inst = state.proto_code(&cl, pc - 2);
2018                        let result_idx = base + prev_inst.arg_a();
2019                        state.set_ci_savedpc(ci, pc);
2020                        state.set_top(state.ci_top(ci));
2021                        state.try_bin_tm(&ra_v, Some(ra_idx), &rb_v, Some(rb_idx), result_idx, tm)?;
2022                        trap = state.ci_trap(ci);
2023                    }
2024                    OpCode::MmBinI => {
2025                        let ra_idx = base + i.arg_a();
2026                        let ra_v = state.get_at(ra_idx);
2027                        let imm = i.arg_s_b() as i64;
2028                        let tm = tagmethod_from_index(i.arg_c() as usize);
2029                        let flip = i.arg_k() != 0;
2030                        let prev_inst = state.proto_code(&cl, pc - 2);
2031                        let result_idx = base + prev_inst.arg_a();
2032                        state.set_ci_savedpc(ci, pc);
2033                        state.set_top(state.ci_top(ci));
2034                        state.try_bin_i_tm(&ra_v, Some(ra_idx), imm, flip, result_idx, tm)?;
2035                        trap = state.ci_trap(ci);
2036                    }
2037                    OpCode::MmBinK => {
2038                        let ra_idx = base + i.arg_a();
2039                        let ra_v = state.get_at(ra_idx);
2040                        let imm = state.proto_const(&cl, i.arg_b() as usize).clone();
2041                        let tm = tagmethod_from_index(i.arg_c() as usize);
2042                        let flip = i.arg_k() != 0;
2043                        let prev_inst = state.proto_code(&cl, pc - 2);
2044                        let result_idx = base + prev_inst.arg_a();
2045                        state.set_ci_savedpc(ci, pc);
2046                        state.set_top(state.ci_top(ci));
2047                        state.try_bin_assoc_tm(&ra_v, Some(ra_idx), &imm, None, flip, result_idx, tm)?;
2048                        trap = state.ci_trap(ci);
2049                    }
2050                    // ── OP_UNM ───────────────────────────────────────────────
2051                    //    else if (tonumberns(rb, nb)) setfltvalue(s2v(ra), -nb)
2052                    //    else Protect(luaT_trybinTM(L, rb, rb, ra, TM_UNM))
2053                    OpCode::Unm => {
2054                        let ra = base + i.arg_a();
2055                        let rb_idx = base + i.arg_b();
2056                        let rb_v = state.get_at(rb_idx);
2057                        match &rb_v {
2058                            LuaValue::Int(ib) => {
2059                                state.set_at(ra, LuaValue::Int(intop_sub(0, *ib)));
2060                            }
2061                            LuaValue::Float(nb) => {
2062                                state.set_at(ra, LuaValue::Float(-nb));
2063                            }
2064                            _ => {
2065                                state.set_ci_savedpc(ci, pc);
2066                                state.set_top(state.ci_top(ci));
2067                                state.try_bin_tm(&rb_v, Some(rb_idx), &rb_v, Some(rb_idx), ra, TagMethod::Unm)?;
2068                                trap = state.ci_trap(ci);
2069                            }
2070                        }
2071                    }
2072                    // ── OP_BNOT ──────────────────────────────────────────────
2073                    OpCode::BNot => {
2074                        let ra = base + i.arg_a();
2075                        let rb_idx = base + i.arg_b();
2076                        let rb_v = state.get_at(rb_idx);
2077                        if let Some(ib) = to_integer_ns(&rb_v, F2Imod::Eq) {
2078                            state.set_at(ra, LuaValue::Int(!ib));
2079                        } else {
2080                            state.set_ci_savedpc(ci, pc);
2081                            state.set_top(state.ci_top(ci));
2082                            state.try_bin_tm(&rb_v, Some(rb_idx), &rb_v, Some(rb_idx), ra, TagMethod::Bnot)?;
2083                            trap = state.ci_trap(ci);
2084                        }
2085                    }
2086                    // ── OP_NOT ───────────────────────────────────────────────
2087                    OpCode::Not => {
2088                        let ra = base + i.arg_a();
2089                        let rb_v = state.get_at(base + i.arg_b());
2090                        let falsy = matches!(rb_v, LuaValue::Nil | LuaValue::Bool(false));
2091                        state.set_at(ra, LuaValue::Bool(falsy));
2092                    }
2093                    // ── OP_LEN ───────────────────────────────────────────────
2094                    OpCode::Len => {
2095                        let ra = base + i.arg_a();
2096                        let rb_v = state.get_at(base + i.arg_b());
2097                        state.set_ci_savedpc(ci, pc);
2098                        state.set_top(state.ci_top(ci));
2099                        obj_len(state, ra, rb_v)?;
2100                        trap = state.ci_trap(ci);
2101                    }
2102                    // ── OP_CONCAT ─────────────────────────────────────────────
2103                    OpCode::Concat => {
2104                        let ra = base + i.arg_a();
2105                        let n = i.arg_b() as i32;
2106                        state.set_top(ra + n as i32);
2107                        state.set_ci_savedpc(ci, pc); // ProtectNT: save pc only
2108                        concat(state, n)?;
2109                        trap = state.ci_trap(ci);
2110                        let top = state.top_idx();
2111                        state.set_ci_savedpc(ci, pc);
2112                        state.set_top(top);
2113                        state.gc_cond_step();
2114                        trap = state.ci_trap(ci);
2115                    }
2116                    // ── OP_CLOSE ──────────────────────────────────────────────
2117                    OpCode::Close => {
2118                        let ra = base + i.arg_a();
2119                        state.set_ci_savedpc(ci, pc);
2120                        state.set_top(state.ci_top(ci));
2121                        crate::func::close(state, ra, lua_types::status::LuaStatus::Ok as i32, true)?;
2122                        trap = state.ci_trap(ci);
2123                    }
2124                    // ── OP_TBC ────────────────────────────────────────────────
2125                    OpCode::Tbc => {
2126                        let ra = base + i.arg_a();
2127                        state.set_ci_savedpc(ci, pc);
2128                        state.set_top(state.ci_top(ci));
2129                        state.new_tbc_upval(ra)?;
2130                    }
2131                    // ── OP_JMP ────────────────────────────────────────────────
2132                    OpCode::Jmp => {
2133                        pc = (pc as i64 + i.arg_s_j() as i64) as u32;
2134                        trap = state.ci_trap(ci);
2135                    }
2136                    // ── OP_EQ ─────────────────────────────────────────────────
2137                    OpCode::Eq => {
2138                        let ra_v = state.get_at(base + i.arg_a());
2139                        let rb_v = state.get_at(base + i.arg_b());
2140                        state.set_ci_savedpc(ci, pc);
2141                        state.set_top(state.ci_top(ci));
2142                        let cond = equal_obj(Some(state), &ra_v, &rb_v)? as u32;
2143                        trap = state.ci_trap(ci);
2144                        if (cond as i32) != i.arg_k() {
2145                            pc += 1;
2146                        } else {
2147                            let next = state.proto_code(&cl, pc);
2148                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2149                            trap = state.ci_trap(ci);
2150                        }
2151                    }
2152                    // ── OP_LT ─────────────────────────────────────────────────
2153                    OpCode::Lt => {
2154                        let ra_v = state.get_at(base + i.arg_a());
2155                        let rb_v = state.get_at(base + i.arg_b());
2156                        let cond = if let (LuaValue::Int(ia), LuaValue::Int(ib)) = (&ra_v, &rb_v) {
2157                            *ia < *ib
2158                        } else if matches!((&ra_v, &rb_v),
2159                            (LuaValue::Int(_) | LuaValue::Float(_),
2160                             LuaValue::Int(_) | LuaValue::Float(_))) {
2161                            lt_num(&ra_v, &rb_v)
2162                        } else {
2163                            state.set_ci_savedpc(ci, pc);
2164                            state.set_top(state.ci_top(ci));
2165                            let r = less_than_others(state, &ra_v, &rb_v)?;
2166                            trap = state.ci_trap(ci);
2167                            r
2168                        };
2169                        if (cond as i32) != i.arg_k() {
2170                            pc += 1;
2171                        } else {
2172                            let next = state.proto_code(&cl, pc);
2173                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2174                            trap = state.ci_trap(ci);
2175                        }
2176                    }
2177                    // ── OP_LE ─────────────────────────────────────────────────
2178                    OpCode::Le => {
2179                        let ra_v = state.get_at(base + i.arg_a());
2180                        let rb_v = state.get_at(base + i.arg_b());
2181                        let cond = if let (LuaValue::Int(ia), LuaValue::Int(ib)) = (&ra_v, &rb_v) {
2182                            *ia <= *ib
2183                        } else if matches!((&ra_v, &rb_v),
2184                            (LuaValue::Int(_) | LuaValue::Float(_),
2185                             LuaValue::Int(_) | LuaValue::Float(_))) {
2186                            le_num(&ra_v, &rb_v)
2187                        } else {
2188                            state.set_ci_savedpc(ci, pc);
2189                            state.set_top(state.ci_top(ci));
2190                            let r = less_equal_others(state, &ra_v, &rb_v)?;
2191                            trap = state.ci_trap(ci);
2192                            r
2193                        };
2194                        if (cond as i32) != i.arg_k() {
2195                            pc += 1;
2196                        } else {
2197                            let next = state.proto_code(&cl, pc);
2198                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2199                            trap = state.ci_trap(ci);
2200                        }
2201                    }
2202                    // ── OP_EQK ────────────────────────────────────────────────
2203                    OpCode::EqK => {
2204                        let ra_v = state.get_at(base + i.arg_a());
2205                        let rb_v = state.proto_const(&cl, i.arg_b() as usize).clone();
2206                        let cond = equal_obj(None, &ra_v, &rb_v)? as u32;
2207                        if (cond as i32) != i.arg_k() {
2208                            pc += 1;
2209                        } else {
2210                            let next = state.proto_code(&cl, pc);
2211                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2212                            trap = state.ci_trap(ci);
2213                        }
2214                    }
2215                    // ── OP_EQI ────────────────────────────────────────────────
2216                    //    if (ttisinteger) cond = ivalue == im
2217                    //    elif (ttisfloat) cond = numeq(fltvalue, cast_num(im))
2218                    //    else cond = 0
2219                    OpCode::EqI => {
2220                        let ra_v = state.get_at(base + i.arg_a());
2221                        let im = i.arg_s_b() as i64;
2222                        let cond: bool = match &ra_v {
2223                            LuaValue::Int(iv) => *iv == im,
2224                            LuaValue::Float(fv) => *fv == im as f64,
2225                            _ => false,
2226                        };
2227                        if (cond as i32) != i.arg_k() {
2228                            pc += 1;
2229                        } else {
2230                            let next = state.proto_code(&cl, pc);
2231                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2232                            trap = state.ci_trap(ci);
2233                        }
2234                    }
2235                    // ── OP_LTI / OP_LEI / OP_GTI / OP_GEI ───────────────────
2236                    //              inv=0/0/1/1, tm=TM_LT/TM_LE/TM_LT/TM_LE)
2237                    OpCode::LtI => {
2238                        let ra = base + i.arg_a();
2239                        let im = i.arg_s_b() as i64;
2240                        let fast_cond = match &state.stack[ra.0 as usize].val {
2241                            LuaValue::Int(ia) => Some(*ia < im),
2242                            LuaValue::Float(fa) => Some(*fa < im as f64),
2243                            _ => None,
2244                        };
2245                        let cond = match fast_cond {
2246                            Some(cond) => cond,
2247                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, false, TagMethod::Lt)?,
2248                        };
2249                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2250                    }
2251                    OpCode::LeI => {
2252                        let ra = base + i.arg_a();
2253                        let im = i.arg_s_b() as i64;
2254                        let fast_cond = match &state.stack[ra.0 as usize].val {
2255                            LuaValue::Int(ia) => Some(*ia <= im),
2256                            LuaValue::Float(fa) => Some(*fa <= im as f64),
2257                            _ => None,
2258                        };
2259                        let cond = match fast_cond {
2260                            Some(cond) => cond,
2261                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, false, TagMethod::Le)?,
2262                        };
2263                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2264                    }
2265                    OpCode::GtI => {
2266                        let ra = base + i.arg_a();
2267                        let im = i.arg_s_b() as i64;
2268                        let fast_cond = match &state.stack[ra.0 as usize].val {
2269                            LuaValue::Int(ia) => Some(*ia > im),
2270                            LuaValue::Float(fa) => Some(*fa > im as f64),
2271                            _ => None,
2272                        };
2273                        let cond = match fast_cond {
2274                            Some(cond) => cond,
2275                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, true, TagMethod::Lt)?,
2276                        };
2277                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2278                    }
2279                    OpCode::GeI => {
2280                        let ra = base + i.arg_a();
2281                        let im = i.arg_s_b() as i64;
2282                        let fast_cond = match &state.stack[ra.0 as usize].val {
2283                            LuaValue::Int(ia) => Some(*ia >= im),
2284                            LuaValue::Float(fa) => Some(*fa >= im as f64),
2285                            _ => None,
2286                        };
2287                        let cond = match fast_cond {
2288                            Some(cond) => cond,
2289                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, true, TagMethod::Le)?,
2290                        };
2291                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2292                    }
2293                    // ── OP_TEST ────────────────────────────────────────────────
2294                    OpCode::Test => {
2295                        let ra_v = state.get_at(base + i.arg_a());
2296                        let cond = !matches!(ra_v, LuaValue::Nil | LuaValue::Bool(false));
2297                        if (cond as i32) != i.arg_k() {
2298                            pc += 1;
2299                        } else {
2300                            let next = state.proto_code(&cl, pc);
2301                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2302                            trap = state.ci_trap(ci);
2303                        }
2304                    }
2305                    // ── OP_TESTSET ─────────────────────────────────────────────
2306                    //    else { setobj2s(L, ra, rb); donextjump(ci); }
2307                    OpCode::TestSet => {
2308                        let ra = base + i.arg_a();
2309                        let rb_v = state.get_at(base + i.arg_b());
2310                        let falsy = matches!(rb_v, LuaValue::Nil | LuaValue::Bool(false));
2311                        if (falsy as i32) == i.arg_k() {
2312                            pc += 1;
2313                        } else {
2314                            state.set_at(ra, rb_v);
2315                            let next = state.proto_code(&cl, pc);
2316                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2317                            trap = state.ci_trap(ci);
2318                        }
2319                    }
2320                    // ── OP_CALL ────────────────────────────────────────────────
2321                    //      updatetrap(ci);
2322                    //    else { ci = newci; goto startfunc; }
2323                    OpCode::Call => {
2324                        let ra = base + i.arg_a();
2325                        let b = i.arg_b();
2326                        let nresults = i.arg_c() as i32 - 1;
2327                        if b != 0 {
2328                            state.set_top(ra + b);
2329                        }
2330                        state.set_ci_savedpc(ci, pc); // savepc
2331                        match state.precall(ra, nresults)? {
2332                            None => {
2333                                // C function — nothing else to do
2334                                trap = state.ci_trap(ci); // updatetrap
2335                            }
2336                            Some(new_ci) => {
2337                                // Lua function — goto startfunc
2338                                ci = new_ci;
2339                                continue 'startfunc;
2340                            }
2341                        }
2342                    }
2343                    // ── OP_TAILCALL ────────────────────────────────────────────
2344                    //      goto startfunc;
2345                    //    else { ci->func.p -= delta; luaD_poscall(L, ci, n);
2346                    //            updatetrap; goto ret; }
2347                    OpCode::TailCall => {
2348                        let ra = base + i.arg_a();
2349                        let b = i.arg_b();
2350                        let nparams1 = i.arg_c();
2351                        let delta = if nparams1 != 0 {
2352                            state.ci_nextraargs(ci) + nparams1 as i32
2353                        } else {
2354                            0
2355                        };
2356                        let top_b: i32 = if b != 0 {
2357                            state.set_top(ra + b);
2358                            b
2359                        } else {
2360                            state.top_idx() - ra
2361                        };
2362                        state.set_ci_savedpc(ci, pc);
2363                        if i.test_k() {
2364                            state.close_upvals_from_base(ci)?;
2365                        }
2366                        let n = state.pretailcall(ci, ra, top_b, delta)?;
2367                        if n < 0 {
2368                            // Lua function — goto startfunc
2369                            continue 'startfunc;
2370                        } else {
2371                            // C function — ci->func.p -= delta; luaD_poscall; goto ret
2372                            state.ci_adjust_func(ci, delta);
2373                            state.poscall(ci, n as u32)?;
2374                            trap = state.ci_trap(ci);
2375                            break 'dispatch; // goto ret
2376                        }
2377                    }
2378                    // ── OP_RETURN ──────────────────────────────────────────────
2379                    //    savepc; if TESTARG_k: close upvals;
2380                    //    if nparams1: ci->func -= nextraargs+nparams1;
2381                    //    L->top.p = ra+n; luaD_poscall; goto ret
2382                    OpCode::Return => {
2383                        let ra = base + i.arg_a();
2384                        let n_raw = i.arg_b() as i32 - 1;
2385                        let nparams1 = i.arg_c();
2386                        let n: u32 = if n_raw < 0 {
2387                            (state.top_idx() - ra) as u32
2388                        } else {
2389                            n_raw as u32
2390                        };
2391                        state.set_ci_savedpc(ci, pc);
2392                        if i.test_k() {
2393                            state.ci_nres_set(ci, n as i32);
2394                            let ci_top = state.ci_top(ci);
2395                            if state.top_idx().0 < ci_top.0 {
2396                                state.set_top(ci_top);
2397                            }
2398                            crate::func::close(state, base, crate::func::CLOSE_K_TOP, true)?;
2399                            trap = state.ci_trap(ci);
2400                            base = state.ci_base(ci); // updatestack
2401                        }
2402                        if nparams1 != 0 {
2403                            let nextraargs = state.ci_nextraargs(ci) as u32;
2404                            state.ci_adjust_func(ci, (nextraargs as i32 + nparams1 as i32));
2405                        }
2406                        state.set_top(ra + n as i32);
2407                        state.poscall(ci, n)?;
2408                        trap = state.ci_trap(ci);
2409                        break 'dispatch; // goto ret
2410                    }
2411                    // ── OP_RETURN0 ─────────────────────────────────────────────
2412                    //    else { L->ci = ci->previous; L->top = base-1;
2413                    //           for (nres = ci->nresults; nres > 0; nres--)
2414                    //             setnilvalue(L->top++) }
2415                    //    goto ret;
2416                    OpCode::Return0 => {
2417                        if state.hookmask == 0 {
2418                            let ci_slot = ci.as_usize();
2419                            let nres = state.call_info[ci_slot].nresults as i32;
2420                            state.ci = state.call_info[ci_slot]
2421                                .previous
2422                                .expect("RETURN0: returning frame has no previous CallInfo");
2423                            state.top = base - 1;
2424                            for _ in 0..nres.max(0) {
2425                                state.push(LuaValue::Nil);
2426                            }
2427                        } else {
2428                            return0_hook(state, ci, base, i, pc, &mut trap)?;
2429                        }
2430                        break 'dispatch; // goto ret
2431                    }
2432                    // ── OP_RETURN1 ─────────────────────────────────────────────
2433                    //    else { nres = ci->nresults; ci = ci->previous; ...handle results... }
2434                    //    goto ret;
2435                    OpCode::Return1 => {
2436                        if state.hookmask == 0 {
2437                            let ci_slot = ci.as_usize();
2438                            let nres = state.call_info[ci_slot].nresults as i32;
2439                            state.ci = state.call_info[ci_slot]
2440                                .previous
2441                                .expect("RETURN1: returning frame has no previous CallInfo");
2442                            if nres == 0 {
2443                                state.top = base - 1;
2444                            } else {
2445                                let ra = base + i.arg_a();
2446                                state.stack[(base - 1).0 as usize].val =
2447                                    state.stack[ra.0 as usize].val; // at least this result
2448                                state.top = base;
2449                                for _ in 1..nres.max(0) {
2450                                    state.push(LuaValue::Nil);
2451                                }
2452                            }
2453                        } else {
2454                            return1_hook(state, ci, base, i, pc, &mut trap)?;
2455                        }
2456                        break 'dispatch; // goto ret
2457                    }
2458                    // ── OP_FORLOOP ─────────────────────────────────────────────
2459                    //    else if (floatforloop(ra)) pc -= GETARG_Bx(i)
2460                    //    updatetrap(ci);
2461                    OpCode::ForLoop => {
2462                        let ra = base + i.arg_a();
2463                        let ra_u = ra.0 as usize;
2464                        if let LuaValue::Int(step) = state.stack[ra_u + 2].val {
2465                            let count = match state.stack[ra_u + 1].val {
2466                                LuaValue::Int(c) => c as u64,
2467                                _ => 0,
2468                            };
2469                            if count > 0 {
2470                                let idx = match state.stack[ra_u].val {
2471                                    LuaValue::Int(x) => x,
2472                                    _ => 0,
2473                                };
2474                                state.stack[ra_u + 1].val = LuaValue::Int((count - 1) as i64);
2475                                let new_idx = intop_add(idx, step);
2476                                state.stack[ra_u].val = LuaValue::Int(new_idx);
2477                                state.stack[ra_u + 3].val = LuaValue::Int(new_idx);
2478                                pc = (pc as i64 - i.arg_bx() as i64) as u32;
2479                            }
2480                        } else if float_for_loop(state, ra) {
2481                            pc = (pc as i64 - i.arg_bx() as i64) as u32;
2482                        }
2483                        trap = state.ci_trap(ci);
2484                    }
2485                    // ── OP_FORPREP ─────────────────────────────────────────────
2486                    OpCode::ForPrep => {
2487                        let ra = base + i.arg_a();
2488                        state.set_ci_savedpc(ci, pc);
2489                        state.set_top(state.ci_top(ci));
2490                        if forprep(state, ra)? {
2491                            pc = (pc as i64 + i.arg_bx() as i64 + 1) as u32;
2492                        }
2493                    }
2494                    // ── OP_TFORPREP ────────────────────────────────────────────
2495                    //    pc += GETARG_Bx(i); i = *pc++; assert(OP_TFORCALL && ra==RA(i));
2496                    //    goto l_tforcall;
2497                    OpCode::TForPrep => {
2498                        let ra = base + i.arg_a();
2499                        state.set_ci_savedpc(ci, pc);
2500                        state.set_top(state.ci_top(ci));
2501                        state.new_tbc_upval(ra + 3)?;
2502                        pc = (pc as i64 + i.arg_bx() as i64) as u32;
2503                        let tfc_i = state.proto_code(&cl, pc);
2504                        pc += 1;
2505                        debug_assert!(tfc_i.opcode() == OpCode::TForCall);
2506                        // inline l_tforcall:
2507                        let tfc_ra = base + tfc_i.arg_a();
2508                        for k in 0..3u32 {
2509                            let v = state.get_at(tfc_ra + k as i32);
2510                            state.set_at(tfc_ra + 4 + k as i32, v);
2511                        }
2512                        state.set_top(tfc_ra + 4 + 3);
2513                        state.set_ci_savedpc(ci, pc);
2514                        state.call_at(tfc_ra + 4, tfc_i.arg_c() as i32)?;
2515                        trap = state.ci_trap(ci);
2516                        base = state.ci_base(ci); // updatestack
2517                        let tfl_i = state.proto_code(&cl, pc);
2518                        pc += 1;
2519                        debug_assert!(tfl_i.opcode() == OpCode::TForLoop);
2520                        let tfl_ra = base + tfl_i.arg_a();
2521                        // inline l_tforloop:
2522                        if !matches!(state.get_at(tfl_ra + 4), LuaValue::Nil) {
2523                            let v = state.get_at(tfl_ra + 4);
2524                            state.set_at(tfl_ra + 2, v);
2525                            pc = (pc as i64 - tfl_i.arg_bx() as i64) as u32;
2526                        }
2527                    }
2528                    // ── OP_TFORCALL ────────────────────────────────────────────
2529                    OpCode::TForCall => {
2530                        let ra = base + i.arg_a();
2531                        for k in 0..3u32 {
2532                            let v = state.get_at(ra + k as i32);
2533                            state.set_at(ra + 4 + k as i32, v);
2534                        }
2535                        state.set_top(ra + 4 + 3);
2536                        state.set_ci_savedpc(ci, pc);
2537                        state.call_at(ra + 4, i.arg_c() as i32)?;
2538                        trap = state.ci_trap(ci);
2539                        base = state.ci_base(ci); // updatestack
2540                        let tfl_i = state.proto_code(&cl, pc);
2541                        pc += 1;
2542                        debug_assert!(tfl_i.opcode() == OpCode::TForLoop);
2543                        let tfl_ra = base + tfl_i.arg_a();
2544                        if !matches!(state.get_at(tfl_ra + 4), LuaValue::Nil) {
2545                            let v = state.get_at(tfl_ra + 4);
2546                            state.set_at(tfl_ra + 2, v);
2547                            pc = (pc as i64 - tfl_i.arg_bx() as i64) as u32;
2548                        }
2549                    }
2550                    // ── OP_TFORLOOP ────────────────────────────────────────────
2551                    OpCode::TForLoop => {
2552                        let ra = base + i.arg_a();
2553                        if !matches!(state.get_at(ra + 4), LuaValue::Nil) {
2554                            let v = state.get_at(ra + 4);
2555                            state.set_at(ra + 2, v);
2556                            pc = (pc as i64 - i.arg_bx() as i64) as u32;
2557                        }
2558                    }
2559                    // ── OP_SETLIST ─────────────────────────────────────────────
2560                    //    if TESTARG_k: last += Ax * (MAXARG_C+1); pc++;
2561                    //    for (; n > 0; n--) h->array[last-1] = val; luaC_barrierback
2562                    OpCode::SetList => {
2563                        let ra = base + i.arg_a();
2564                        let n_raw = i.arg_b();
2565                        let mut last = i.arg_c();
2566                        let t_val = state.get_at(ra);
2567                        let n: i32 = if n_raw == 0 {
2568                            state.top_idx() - ra - 1
2569                        } else {
2570                            state.set_top(state.ci_top(ci));
2571                            n_raw
2572                        };
2573                        last += n;
2574                        if i.test_k() {
2575                            let extra = state.proto_code(&cl, pc);
2576                            pc += 1;
2577                            const MAXARG_C: i32 = (1 << 8) - 1;
2578                            last += extra.arg_ax() * (MAXARG_C + 1);
2579                        }
2580                        state.table_ensure_array(&t_val, last as usize)?;
2581                        for k in (1..=n).rev() {
2582                            let val = state.get_at(ra + k as i32);
2583                            state.table_array_set(&t_val, (last - 1) as usize, val.clone())?;
2584                            last -= 1;
2585                            state.gc_barrier_back(&t_val, &val);
2586                        }
2587                    }
2588                    // ── OP_CLOSURE ─────────────────────────────────────────────
2589                    //    halfProtect(pushclosure(L, p, cl->upvals, base, ra));
2590                    //    checkGC(L, ra+1);
2591                    OpCode::Closure => {
2592                        let ra = base + i.arg_a();
2593                        let proto_idx = i.arg_bx() as usize;
2594                        state.set_ci_savedpc(ci, pc);
2595                        state.set_top(state.ci_top(ci));
2596                        push_closure(state, proto_idx, ci, base, ra)?;
2597                        // checkGC
2598                        state.set_ci_savedpc(ci, pc);
2599                        state.set_top(ra + 1);
2600                        state.gc_cond_step();
2601                        trap = state.ci_trap(ci);
2602                    }
2603                    // ── OP_VARARG ──────────────────────────────────────────────
2604                    OpCode::VarArg => {
2605                        let ra = base + i.arg_a();
2606                        let n = i.arg_c() as i32 - 1;
2607                        state.set_ci_savedpc(ci, pc);
2608                        state.set_top(state.ci_top(ci));
2609                        state.get_varargs(ci, ra, n)?;
2610                        trap = state.ci_trap(ci);
2611                    }
2612                    // ── OP_VARARGPREP ──────────────────────────────────────────
2613                    //    if (trap) luaD_hookcall(L, ci); L->oldpc = 1;
2614                    //    updatebase(ci);
2615                    OpCode::VarArgPrep => {
2616                        let nparams = i.arg_a();
2617                        state.set_ci_savedpc(ci, pc);
2618                        state.adjust_varargs(ci, nparams, &cl)?;
2619                        trap = state.ci_trap(ci);
2620                        if trap {
2621                            state.hook_call(ci)?;
2622                            state.set_oldpc(1);
2623                        }
2624                        base = state.ci_base(ci);
2625                    }
2626                    // ── OP_EXTRAARG ────────────────────────────────────────────
2627                    OpCode::ExtraArg => {
2628                        debug_assert!(false, "OP_EXTRAARG executed directly");
2629                    }
2630                    // Unknown opcode
2631                    #[allow(unreachable_patterns)]
2632                    _ => {
2633                        // TODO(port): unrecognised opcode {:?} — add to match
2634                        todo!("unrecognised opcode");
2635                    }
2636                } // end match opcode
2637            } // end 'dispatch loop
2638
2639            // ── ret: label ──────────────────────────────────────────────────
2640            if state.ci_is_fresh(ci) {
2641                return Ok(());
2642            } else {
2643                ci = state.ci_previous(ci).expect("ci_previous: not fresh frame must have previous");
2644                continue 'returning;
2645            }
2646        } // end 'returning loop
2647    } // end 'startfunc loop
2648}
2649
2650// ─── Local opcode dispatch helpers ───────────────────────────────────────────
2651
2652#[inline(always)]
2653fn number_value(v: LuaValue) -> Option<f64> {
2654    match v {
2655        LuaValue::Float(f) => Some(f),
2656        LuaValue::Int(i) => Some(i as f64),
2657        _ => None,
2658    }
2659}
2660
2661/// Increments `pc` on success (the `pc++` in the C macros).
2662#[allow(dead_code)]
2663#[inline]
2664fn arith_op_aux_rr(
2665    state: &mut LuaState,
2666    ra: StackIdx,
2667    v1: &LuaValue,
2668    v2: &LuaValue,
2669    pc: &mut u32,
2670    iop: fn(i64, i64) -> i64,
2671    fop: fn(f64, f64) -> f64,
2672) {
2673    if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (v1, v2) {
2674        *pc += 1;
2675        state.set_at(ra, LuaValue::Int(iop(*i1, *i2)));
2676    } else {
2677        arith_float_aux(state, ra, v1, v2, pc, fop);
2678    }
2679}
2680
2681#[allow(dead_code)]
2682#[inline]
2683fn arith_float_aux(
2684    state: &mut LuaState,
2685    ra: StackIdx,
2686    v1: &LuaValue,
2687    v2: &LuaValue,
2688    pc: &mut u32,
2689    fop: fn(f64, f64) -> f64,
2690) {
2691    let n1 = match v1 {
2692        LuaValue::Float(f) => Some(*f),
2693        LuaValue::Int(i) => Some(*i as f64),
2694        _ => None,
2695    };
2696    let n2 = match v2 {
2697        LuaValue::Float(f) => Some(*f),
2698        LuaValue::Int(i) => Some(*i as f64),
2699        _ => None,
2700    };
2701    if let (Some(n1), Some(n2)) = (n1, n2) {
2702        *pc += 1;
2703        state.set_at(ra, LuaValue::Float(fop(n1, n2)));
2704    }
2705}
2706
2707#[allow(dead_code)]
2708#[inline]
2709fn arith_op_checked(
2710    state: &mut LuaState,
2711    ra: StackIdx,
2712    v1: &LuaValue,
2713    v2: &LuaValue,
2714    pc: &mut u32,
2715    iop: fn(i64, i64) -> Result<i64, LuaError>,
2716    fop: fn(f64, f64) -> f64,
2717) -> Result<(), LuaError> {
2718    if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (v1, v2) {
2719        *pc += 1;
2720        let result = iop(*i1, *i2).map_err(|e| match e {
2721            LuaError::Runtime(LuaValue::Str(s)) => {
2722                crate::debug::prefixed_runtime_pub(state, s.as_bytes().to_vec())
2723            }
2724            other => other,
2725        })?;
2726        state.set_at(ra, LuaValue::Int(result));
2727    } else {
2728        arith_float_aux(state, ra, v1, v2, pc, fop);
2729    }
2730    Ok(())
2731}
2732
2733#[allow(dead_code)]
2734#[inline]
2735fn bitwise_op_k(
2736    state: &mut LuaState,
2737    ra: StackIdx,
2738    v1: &LuaValue,
2739    v2: &LuaValue, // must be integer (K constant)
2740    pc: &mut u32,
2741    op: fn(i64, i64) -> i64,
2742) {
2743    let i2 = match v2 {
2744        LuaValue::Int(i) => *i,
2745        _ => return,
2746    };
2747    if let Some(i1) = to_integer_ns(v1, F2Imod::Eq) {
2748        *pc += 1;
2749        state.set_at(ra, LuaValue::Int(op(i1, i2)));
2750    }
2751}
2752
2753#[allow(dead_code)]
2754#[inline]
2755fn bitwise_op_rr(
2756    state: &mut LuaState,
2757    ra: StackIdx,
2758    v1: &LuaValue,
2759    v2: &LuaValue,
2760    pc: &mut u32,
2761    op: fn(i64, i64) -> i64,
2762) {
2763    if let (Some(i1), Some(i2)) = (
2764        to_integer_ns(v1, F2Imod::Eq),
2765        to_integer_ns(v2, F2Imod::Eq),
2766    ) {
2767        *pc += 1;
2768        state.set_at(ra, LuaValue::Int(op(i1, i2)));
2769    }
2770}
2771
2772/// `right = true` negates `y` for right-shift semantics.
2773#[allow(dead_code)]
2774#[inline]
2775fn bitwise_shift_rr(
2776    state: &mut LuaState,
2777    ra: StackIdx,
2778    v1: &LuaValue,
2779    v2: &LuaValue,
2780    pc: &mut u32,
2781    right: bool,
2782) {
2783    if let (Some(i1), Some(i2)) = (
2784        to_integer_ns(v1, F2Imod::Eq),
2785        to_integer_ns(v2, F2Imod::Eq),
2786    ) {
2787        let y = if right { intop_sub(0, i2) } else { i2 };
2788        *pc += 1;
2789        state.set_at(ra, LuaValue::Int(shiftl(i1, y)));
2790    }
2791}
2792
2793/// Cold half of C's `op_orderI` macro: only reached when the operand is not a
2794/// plain integer/float and a metamethod lookup may be needed.
2795#[cold]
2796#[inline(never)]
2797#[allow(clippy::too_many_arguments)]
2798fn order_imm_slow(
2799    state: &mut LuaState,
2800    ra: StackIdx,
2801    pc: u32,
2802    trap: &mut bool,
2803    ci: CallInfoIdx,
2804    i: Instruction,
2805    im: i64,
2806    inv: bool,
2807    tm: TagMethod,
2808) -> Result<bool, LuaError> {
2809    let ra_v = state.get_at(ra);
2810    let isf = i.arg_c() != 0;
2811    state.set_ci_savedpc(ci, pc);
2812    state.set_top(state.ci_top(ci));
2813    let r = state.call_order_i_tm(&ra_v, im, inv, isf, tm)?;
2814    *trap = state.ci_trap(ci);
2815    Ok(r)
2816}
2817
2818#[inline(always)]
2819fn finish_order_imm_jump(
2820    state: &mut LuaState,
2821    cl: &lua_types::GcRef<lua_types::LuaLClosure>,
2822    pc: &mut u32,
2823    trap: &mut bool,
2824    ci: CallInfoIdx,
2825    i: Instruction,
2826    cond: bool,
2827) {
2828    if (cond as i32) != i.arg_k() {
2829        *pc += 1;
2830    } else {
2831        let next = state.proto_code(&cl, *pc);
2832        *pc = (*pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2833        *trap = state.ci_trap(ci);
2834    }
2835}
2836
2837#[cold]
2838#[inline(never)]
2839fn return0_hook(
2840    state: &mut LuaState,
2841    ci: CallInfoIdx,
2842    base: StackIdx,
2843    i: Instruction,
2844    pc: u32,
2845    trap: &mut bool,
2846) -> Result<(), LuaError> {
2847    let ra = base + i.arg_a();
2848    state.set_top(ra);
2849    state.set_ci_savedpc(ci, pc);
2850    state.poscall(ci, 0)?;
2851    *trap = true;
2852    Ok(())
2853}
2854
2855#[cold]
2856#[inline(never)]
2857fn return1_hook(
2858    state: &mut LuaState,
2859    ci: CallInfoIdx,
2860    base: StackIdx,
2861    i: Instruction,
2862    pc: u32,
2863    trap: &mut bool,
2864) -> Result<(), LuaError> {
2865    let ra = base + i.arg_a();
2866    state.set_top(ra + 1);
2867    state.set_ci_savedpc(ci, pc);
2868    state.poscall(ci, 1)?;
2869    *trap = true;
2870    Ok(())
2871}
2872
2873// ──────────────────────────────────────────────────────────────────────────
2874// PORT STATUS
2875//   source:        src/lvm.c  (1899 lines, 32 functions)
2876//   target_crate:  lua-vm
2877//   confidence:    medium
2878//   todos:         6
2879//   port_notes:    4
2880//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
2881//   notes:         All opcode handlers and helpers translated; LuaState methods
2882//                  referenced (fast_get, precall, poscall, etc.) are stubs that
2883//                  Phase B will land.  The execute() goto flow is modelled with
2884//                  labelled Rust loops ('startfunc/'returning/'dispatch).
2885//                  str_to_number is a stub pending luaO_str2num port (TODO #1).
2886//                  strcoll replaced with byte-lexicographic order (TODO #2).
2887//                  order_imm_op uses LuaValue as a stand-in for GcRef<LuaClosure>
2888//                  (TODO #3).  ClosureRef type alias not yet defined (TODO #4-6).
2889// ──────────────────────────────────────────────────────────────────────────