Skip to main content

lua_vm/
vm.rs

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