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