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_value_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_value_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 ci_slot = ci.as_usize();
1681            let func_idx = state.call_info[ci_slot].func;
1682            let cl = match state.stack.get(func_idx.0 as usize).map(|slot| slot.val) {
1683                Some(LuaValue::Function(lua_types::closure::LuaClosure::Lua(c))) => c,
1684                _ => {
1685                    return Err(LuaError::runtime(format_args!(
1686                        "internal: execute called on non-Lua frame"
1687                    )));
1688                }
1689            };
1690            let code = &cl.proto.code;
1691            let constants = &cl.proto.k;
1692            // pc is an index into proto.code (u32)
1693            let mut pc: u32 = state.call_info[ci_slot].saved_pc();
1694
1695            if trap {
1696                trap = state.trace_call(ci)?;
1697            }
1698            let mut base: StackIdx = state.call_info[ci.as_usize()].func + 1;
1699
1700            // ── Main dispatch loop ──────────────────────────────────────────
1701            'dispatch: loop {
1702                if trap {
1703                    trap = state.trace_exec(ci, pc)?;
1704                    base = state.ci_base(ci); // updatebase
1705                }
1706                let i: Instruction = code[pc as usize];
1707                pc += 1;
1708                let op = i.opcode();
1709                #[cfg(feature = "opcode-profile")]
1710                crate::opcode_profile::record(op);
1711
1712                debug_assert!(base == state.ci_base(ci));
1713
1714                // In normal C-Lua builds, `lua_assert` compiles away; keep the
1715                // stack-top invalidation only for debug parity so release
1716                // dispatch avoids an opcode-mode lookup and a `top` write.
1717                #[cfg(debug_assertions)]
1718                {
1719                    let op_mode = op_mode_byte(op);
1720                    if (op_mode & (1 << 5)) == 0 || i.arg_b() != 0 {
1721                        state.set_top(base);
1722                    }
1723                }
1724
1725                match op {
1726                    // ── OP_MOVE ──────────────────────────────────────────────
1727                    OpCode::Move => {
1728                        let ra = base + i.arg_a();
1729                        let rb = base + i.arg_b();
1730                        let v = state.stack[rb.0 as usize].val;
1731                        state.stack[ra.0 as usize].val = v;
1732                    }
1733                    // ── OP_LOADI ─────────────────────────────────────────────
1734                    OpCode::LoadI => {
1735                        let ra = base + i.arg_a();
1736                        let b = i.arg_s_bx() as i64;
1737                        state.stack[ra.0 as usize].val = LuaValue::Int(b);
1738                    }
1739                    // ── OP_LOADF ─────────────────────────────────────────────
1740                    OpCode::LoadF => {
1741                        let ra = base + i.arg_a();
1742                        let b = i.arg_s_bx() as f64;
1743                        state.stack[ra.0 as usize].val = LuaValue::Float(b);
1744                    }
1745                    // ── OP_LOADK ─────────────────────────────────────────────
1746                    OpCode::LoadK => {
1747                        let ra = base + i.arg_a();
1748                        let k_idx = i.arg_bx() as usize;
1749                        state.stack[ra.0 as usize].val = constants[k_idx];
1750                    }
1751                    // ── OP_LOADKX ────────────────────────────────────────────
1752                    OpCode::LoadKX => {
1753                        let ra = base + i.arg_a();
1754                        let extra = code[pc as usize];
1755                        pc += 1;
1756                        let k_idx = extra.arg_ax() as usize;
1757                        state.stack[ra.0 as usize].val = constants[k_idx];
1758                    }
1759                    // ── OP_LOADFALSE ─────────────────────────────────────────
1760                    OpCode::LoadFalse => {
1761                        let ra = base + i.arg_a();
1762                        state.stack[ra.0 as usize].val = LuaValue::Bool(false);
1763                    }
1764                    // ── OP_LFALSESKIP ────────────────────────────────────────
1765                    OpCode::LFalseSkip => {
1766                        let ra = base + i.arg_a();
1767                        state.stack[ra.0 as usize].val = LuaValue::Bool(false);
1768                        pc += 1;
1769                    }
1770                    // ── OP_LOADTRUE ──────────────────────────────────────────
1771                    OpCode::LoadTrue => {
1772                        let ra = base + i.arg_a();
1773                        state.stack[ra.0 as usize].val = LuaValue::Bool(true);
1774                    }
1775                    // ── OP_LOADNIL ───────────────────────────────────────────
1776                    OpCode::LoadNil => {
1777                        let ra = base + i.arg_a();
1778                        let b = i.arg_b();
1779                        for k in 0..=b {
1780                            state.stack[(ra + k).0 as usize].val = LuaValue::Nil;
1781                        }
1782                    }
1783                    // ── OP_GETUPVAL ──────────────────────────────────────────
1784                    OpCode::GetUpVal => {
1785                        let ra = base + i.arg_a();
1786                        let b = i.arg_b() as usize;
1787                        let uv = cl.upval(b);
1788                        let v = match uv.try_open_payload() {
1789                            Some((thread_id, idx)) if thread_id as u64 == state.cached_thread_id => {
1790                                state.stack[idx.0 as usize].val
1791                            }
1792                            Some(_) => state.upvalue_get(&cl, b),
1793                            None => uv.closed_value(),
1794                        };
1795                        state.stack[ra.0 as usize].val = v;
1796                    }
1797                    // ── OP_SETUPVAL ──────────────────────────────────────────
1798                    //    setobj(L, uv->v.p, s2v(ra)); luaC_barrier(L, uv, s2v(ra));
1799                    OpCode::SetUpVal => {
1800                        let ra = base + i.arg_a();
1801                        let b = i.arg_b() as usize;
1802                        let v = state.stack[ra.0 as usize].val;
1803                        let uv = cl.upval(b);
1804                        match uv.try_open_payload() {
1805                            Some((thread_id, idx)) if thread_id as u64 == state.cached_thread_id => {
1806                                state.stack[idx.0 as usize].val = v;
1807                                if v.is_collectable() {
1808                                    state.gc_barrier_upval(&uv, &v);
1809                                }
1810                            }
1811                            None => {
1812                                uv.set_closed_value(v);
1813                                if v.is_collectable() {
1814                                    state.gc_barrier_upval(&uv, &v);
1815                                }
1816                            }
1817                            _ => {
1818                                state.upvalue_set(&cl, b, v)?;
1819                            }
1820                        }
1821                    }
1822                    // ── OP_GETTABUP ──────────────────────────────────────────
1823                    //    if (luaV_fastget(..., luaH_getshortstr)) setobj2s(L, ra, slot)
1824                    //    else Protect(luaV_finishget(...))
1825                    OpCode::GetTabUp => {
1826                        let ra = base + i.arg_a();
1827                        let b = i.arg_b() as usize;
1828                        let k_idx = i.arg_c() as usize;
1829                        let upval = state.upvalue_get(&cl, b);
1830                        let key = constants[k_idx];
1831                        match state.fast_get_short_str(&upval, &key)? {
1832                            Some(v) => state.set_at(ra, v),
1833                            None => {
1834                                state.set_ci_savedpc(ci, pc);
1835                                state.set_top(state.ci_top(ci));
1836                                finish_get(state, upval, key, ra, true, None)?;
1837                                trap = state.ci_trap(ci);
1838                            }
1839                        }
1840                    }
1841                    // ── OP_GETTABLE ──────────────────────────────────────────
1842                    //    if (integer key) fastgeti else fastget
1843                    OpCode::GetTable => {
1844                        let ra = base + i.arg_a();
1845                        let rb_idx = base + i.arg_b();
1846                        let rb_v = state.get_at(rb_idx);
1847                        let rc_v = state.get_at(base + i.arg_c());
1848                        let fast_result = if let LuaValue::Int(n) = &rc_v {
1849                            state.fast_get_int(&rb_v, *n)?
1850                        } else {
1851                            state.fast_get(&rb_v, &rc_v)?
1852                        };
1853                        match fast_result {
1854                            Some(v) => state.set_at(ra, v),
1855                            None => {
1856                                state.set_ci_savedpc(ci, pc);
1857                                state.set_top(state.ci_top(ci));
1858                                finish_get(state, rb_v, rc_v, ra, true, Some(rb_idx))?;
1859                                trap = state.ci_trap(ci);
1860                            }
1861                        }
1862                    }
1863                    // ── OP_GETI ──────────────────────────────────────────────
1864                    //    if (luaV_fastgeti(L, rb, c, slot)) setobj2s(L, ra, slot)
1865                    //    else { TValue key; setivalue(&key, c); Protect(finishget) }
1866                    OpCode::GetI => {
1867                        let ra = base + i.arg_a();
1868                        let rb_idx = base + i.arg_b();
1869                        let rb_v = state.get_at(rb_idx);
1870                        let c = i.arg_c() as i64;
1871                        match state.fast_get_int(&rb_v, c)? {
1872                            Some(v) => state.set_at(ra, v),
1873                            None => {
1874                                let key = LuaValue::Int(c);
1875                                state.set_ci_savedpc(ci, pc);
1876                                state.set_top(state.ci_top(ci));
1877                                finish_get(state, rb_v, key, ra, true, Some(rb_idx))?;
1878                                trap = state.ci_trap(ci);
1879                            }
1880                        }
1881                    }
1882                    // ── OP_GETFIELD ──────────────────────────────────────────
1883                    OpCode::GetField => {
1884                        let ra = base + i.arg_a();
1885                        let rb_idx = base + i.arg_b();
1886                        let rb_v = state.get_at(rb_idx);
1887                        let k_idx = i.arg_c() as usize;
1888                        let key = constants[k_idx];
1889                        match state.fast_get_short_str(&rb_v, &key)? {
1890                            Some(v) => state.set_at(ra, v),
1891                            None => {
1892                                state.set_ci_savedpc(ci, pc);
1893                                state.set_top(state.ci_top(ci));
1894                                finish_get(state, rb_v, key, ra, true, Some(rb_idx))?;
1895                                trap = state.ci_trap(ci);
1896                            }
1897                        }
1898                    }
1899                    // ── OP_SETTABUP ──────────────────────────────────────────
1900                    OpCode::SetTabUp => {
1901                        let a = i.arg_a() as usize;
1902                        let b_idx = i.arg_b() as usize; // key is KB(i)
1903                        let rc_v = if i.test_k() {
1904                            constants[i.arg_c() as usize]
1905                        } else {
1906                            state.get_at(base + i.arg_c())
1907                        };
1908                        let upval = state.upvalue_get(&cl, a);
1909                        let key = constants[b_idx];
1910                        match state.fast_get_short_str(&upval, &key)? {
1911                            Some(_slot) => {
1912                                state.table_raw_set(&upval, key, rc_v.clone())?;
1913                                state.gc_value_barrier_back(&upval, &rc_v);
1914                            }
1915                            None => {
1916                                state.set_ci_savedpc(ci, pc);
1917                                state.set_top(state.ci_top(ci));
1918                                let upval_name: Vec<u8> = cl
1919                                    .proto
1920                                    .upvalues
1921                                    .get(a)
1922                                    .and_then(|uv| uv.name.as_ref())
1923                                    .map(|s| s.as_bytes().to_vec())
1924                                    .unwrap_or_else(|| b"?".to_vec());
1925                                let hint: Option<(&[u8], &[u8])> =
1926                                    Some((b"upvalue", &upval_name));
1927                                finish_set(state, upval, key, rc_v, false, None, hint)?;
1928                                trap = state.ci_trap(ci);
1929                            }
1930                        }
1931                    }
1932                    // ── OP_SETTABLE ───────────────────────────────────────────
1933                    OpCode::SetTable => {
1934                        let ra_idx = base + i.arg_a();
1935                        let ra_v = state.get_at(ra_idx);
1936                        let rb_v = state.get_at(base + i.arg_b());
1937                        let rc_v = if i.test_k() {
1938                            constants[i.arg_c() as usize]
1939                        } else {
1940                            state.get_at(base + i.arg_c())
1941                        };
1942                        if let LuaValue::Table(tbl) = ra_v {
1943                            if tbl.metatable().is_none() {
1944                                state.gc_table_barrier_back(&tbl, &rc_v);
1945                                tbl.raw_set(state, rb_v, rc_v)?;
1946                            } else {
1947                                let fast = if let LuaValue::Int(n) = &rb_v {
1948                                    state.fast_get_int(&ra_v, *n)?
1949                                } else {
1950                                    state.fast_get(&ra_v, &rb_v)?
1951                                };
1952                                if fast.is_some() {
1953                                    state.table_raw_set(&ra_v, rb_v, rc_v.clone())?;
1954                                    state.gc_value_barrier_back(&ra_v, &rc_v);
1955                                } else {
1956                                    state.set_ci_savedpc(ci, pc);
1957                                    state.set_top(state.ci_top(ci));
1958                                    finish_set(state, ra_v, rb_v, rc_v, false, Some(ra_idx), None)?;
1959                                    trap = state.ci_trap(ci);
1960                                }
1961                            }
1962                        } else {
1963                            state.set_ci_savedpc(ci, pc);
1964                            state.set_top(state.ci_top(ci));
1965                            finish_set(state, ra_v, rb_v, rc_v, false, Some(ra_idx), None)?;
1966                            trap = state.ci_trap(ci);
1967                        }
1968                    }
1969                    // ── OP_SETI ───────────────────────────────────────────────
1970                    OpCode::SetI => {
1971                        let ra_idx = base + i.arg_a();
1972                        let ra_v = state.get_at(ra_idx);
1973                        let c = i.arg_b() as i64;
1974                        let rc_v = if i.test_k() {
1975                            constants[i.arg_c() as usize]
1976                        } else {
1977                            state.get_at(base + i.arg_c())
1978                        };
1979                        if let LuaValue::Table(tbl) = ra_v {
1980                            if tbl.metatable().is_none() {
1981                                state.gc_table_barrier_back(&tbl, &rc_v);
1982                                tbl.raw_set_int(state, c, rc_v)?;
1983                            } else {
1984                                let fast = state.fast_get_int(&ra_v, c)?;
1985                                if fast.is_some() {
1986                                    state.table_raw_set(&ra_v, LuaValue::Int(c), rc_v.clone())?;
1987                                    state.gc_value_barrier_back(&ra_v, &rc_v);
1988                                } else {
1989                                    state.set_ci_savedpc(ci, pc);
1990                                    state.set_top(state.ci_top(ci));
1991                                    finish_set(state, ra_v, LuaValue::Int(c), rc_v, false, Some(ra_idx), None)?;
1992                                    trap = state.ci_trap(ci);
1993                                }
1994                            }
1995                        } else {
1996                            state.set_ci_savedpc(ci, pc);
1997                            state.set_top(state.ci_top(ci));
1998                            finish_set(state, ra_v, LuaValue::Int(c), rc_v, false, Some(ra_idx), None)?;
1999                            trap = state.ci_trap(ci);
2000                        }
2001                    }
2002                    // ── OP_SETFIELD ───────────────────────────────────────────
2003                    OpCode::SetField => {
2004                        let ra_idx = base + i.arg_a();
2005                        let ra_v = state.get_at(ra_idx);
2006                        let b_idx = i.arg_b() as usize;
2007                        let key = constants[b_idx];
2008                        let rc_v = if i.test_k() {
2009                            constants[i.arg_c() as usize]
2010                        } else {
2011                            state.get_at(base + i.arg_c())
2012                        };
2013                        if let LuaValue::Table(tbl) = ra_v {
2014                            if tbl.metatable().is_none() {
2015                                state.gc_table_barrier_back(&tbl, &rc_v);
2016                                tbl.raw_set(state, key, rc_v)?;
2017                            } else {
2018                                match state.fast_get_short_str(&ra_v, &key)? {
2019                                    Some(_) => {
2020                                        state.table_raw_set(&ra_v, key, rc_v.clone())?;
2021                                        state.gc_value_barrier_back(&ra_v, &rc_v);
2022                                    }
2023                                    None => {
2024                                        state.set_ci_savedpc(ci, pc);
2025                                        state.set_top(state.ci_top(ci));
2026                                        finish_set(state, ra_v, key, rc_v, false, Some(ra_idx), None)?;
2027                                        trap = state.ci_trap(ci);
2028                                    }
2029                                }
2030                            }
2031                        } else {
2032                            state.set_ci_savedpc(ci, pc);
2033                            state.set_top(state.ci_top(ci));
2034                            finish_set(state, ra_v, key, rc_v, false, Some(ra_idx), None)?;
2035                            trap = state.ci_trap(ci);
2036                        }
2037                    }
2038                    // ── OP_NEWTABLE ───────────────────────────────────────────
2039                    //    if (TESTARG_k(i)) c += GETARG_Ax(*pc) * (MAXARG_C + 1); pc++;
2040                    OpCode::NewTable => {
2041                        let ra = base + i.arg_a();
2042                        let mut b = i.arg_b();
2043                        let mut c = i.arg_c();
2044                        if b > 0 {
2045                            b = 1 << (b - 1);
2046                        }
2047                        if i.test_k() {
2048                            let extra = code[pc as usize];
2049                            pc += 1;
2050                            const MAXARG_C: i32 = (1 << 8) - 1;
2051                            c += extra.arg_ax() * (MAXARG_C + 1);
2052                        } else {
2053                            pc += 1; // skip extra argument even if zero
2054                        }
2055                        state.set_top(ra + 1);
2056                        let t = if b != 0 || c != 0 {
2057                            state.new_table_with_sizes(c as u32, b as u32)?
2058                        } else {
2059                            state.new_table()
2060                        };
2061                        state.set_at(ra, LuaValue::Table(t.clone()));
2062                        state.set_ci_savedpc(ci, pc);
2063                        state.set_top(ra + 1);
2064                        state.gc_cond_step();
2065                        if state.hookmask != 0 {
2066                            trap = state.ci_trap(ci);
2067                        }
2068                    }
2069                    // ── OP_SELF ───────────────────────────────────────────────
2070                    OpCode::Self_ => {
2071                        let ra = base + i.arg_a();
2072                        let rb_idx = base + i.arg_b();
2073                        let rb_v = state.get_at(rb_idx);
2074                        let k_idx = i.arg_c() as usize; // RKC key (always a string)
2075                        let key = if i.test_k() {
2076                            constants[k_idx]
2077                        } else {
2078                            state.get_at(base + i.arg_c())
2079                        };
2080                        state.set_at(ra + 1, rb_v.clone());
2081                        match state.fast_get_short_str(&rb_v, &key)? {
2082                            Some(v) => state.set_at(ra, v),
2083                            None => {
2084                                state.set_ci_savedpc(ci, pc);
2085                                state.set_top(state.ci_top(ci));
2086                                finish_get(state, rb_v, key, ra, true, Some(rb_idx))?;
2087                                trap = state.ci_trap(ci);
2088                            }
2089                        }
2090                    }
2091                    // ── Arithmetic immediates ──────────────────────────────────
2092                    OpCode::AddI => {
2093                        let ra = base + i.arg_a();
2094                        let rb = base + i.arg_b();
2095                        let imm = i.arg_s_c() as i64;
2096                        let rb_v = state.stack[rb.0 as usize].val;
2097                        match rb_v {
2098                            LuaValue::Int(iv1) => {
2099                                pc += 1;
2100                                state.stack[ra.0 as usize].val = LuaValue::Int(intop_add(iv1, imm));
2101                            }
2102                            LuaValue::Float(nb) => {
2103                                pc += 1;
2104                                state.stack[ra.0 as usize].val = LuaValue::Float(nb + imm as f64);
2105                            }
2106                            _ => {}
2107                        }
2108                    }
2109                    // ── Arithmetic with K constant operand ─────────────────────
2110                    OpCode::AddK => {
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(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
2115                            pc += 1;
2116                            state.set_at(ra, LuaValue::Int(intop_add(i1, i2)));
2117                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
2118                            pc += 1;
2119                            state.set_at(ra, LuaValue::Float(n1 + n2));
2120                        }
2121                    }
2122                    OpCode::SubK => {
2123                        let ra = base + i.arg_a();
2124                        let rb = base + i.arg_b();
2125                        let kidx = i.arg_c() as usize;
2126                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
2127                            pc += 1;
2128                            state.set_at(ra, LuaValue::Int(intop_sub(i1, i2)));
2129                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
2130                            pc += 1;
2131                            state.set_at(ra, LuaValue::Float(n1 - n2));
2132                        }
2133                    }
2134                    OpCode::MulK => {
2135                        let ra = base + i.arg_a();
2136                        let rb = base + i.arg_b();
2137                        let kidx = i.arg_c() as usize;
2138                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
2139                            pc += 1;
2140                            state.set_at(ra, LuaValue::Int(intop_mul(i1, i2)));
2141                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
2142                            pc += 1;
2143                            state.set_at(ra, LuaValue::Float(n1 * n2));
2144                        }
2145                    }
2146                    OpCode::ModK => {
2147                        let ra = base + i.arg_a();
2148                        let v1 = state.get_at(base + i.arg_b());
2149                        let v2 = constants[i.arg_c() as usize];
2150                        state.set_ci_savedpc(ci, pc); // savestate for div-by-zero
2151                        state.set_top(state.ci_top(ci));
2152                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
2153                            |a, b| imod(a, b), fmodf)?;
2154                    }
2155                    OpCode::PowK => {
2156                        let ra = base + i.arg_a();
2157                        let rb = base + i.arg_b();
2158                        let kidx = i.arg_c() as usize;
2159                        if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
2160                            pc += 1;
2161                            let r = if n2 == 2.0 { n1 * n1 } else { n1.powf(n2) };
2162                            state.set_at(ra, LuaValue::Float(r));
2163                        }
2164                    }
2165                    OpCode::DivK => {
2166                        let ra = base + i.arg_a();
2167                        let rb = base + i.arg_b();
2168                        let kidx = i.arg_c() as usize;
2169                        if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
2170                            pc += 1;
2171                            state.set_at(ra, LuaValue::Float(n1 / n2));
2172                        }
2173                    }
2174                    OpCode::IDivK => {
2175                        let ra = base + i.arg_a();
2176                        let v1 = state.get_at(base + i.arg_b());
2177                        let v2 = constants[i.arg_c() as usize];
2178                        state.set_ci_savedpc(ci, pc);
2179                        state.set_top(state.ci_top(ci));
2180                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
2181                            |a, b| idiv(a, b), |a, b| (a / b).floor())?;
2182                    }
2183                    OpCode::BAndK => {
2184                        let ra = base + i.arg_a();
2185                        let v1 = state.get_at(base + i.arg_b());
2186                        let v2 = constants[i.arg_c() as usize];
2187                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_band);
2188                    }
2189                    OpCode::BOrK => {
2190                        let ra = base + i.arg_a();
2191                        let v1 = state.get_at(base + i.arg_b());
2192                        let v2 = constants[i.arg_c() as usize];
2193                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_bor);
2194                    }
2195                    OpCode::BXOrK => {
2196                        let ra = base + i.arg_a();
2197                        let v1 = state.get_at(base + i.arg_b());
2198                        let v2 = constants[i.arg_c() as usize];
2199                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_bxor);
2200                    }
2201                    OpCode::ShrI => {
2202                        let ra = base + i.arg_a();
2203                        let v = state.get_at(base + i.arg_b());
2204                        let ic = i.arg_s_c() as i64;
2205                        if let Some(ib) = to_integer_ns(&v, F2Imod::Eq) {
2206                            pc += 1;
2207                            state.set_at(ra, LuaValue::Int(shiftl(ib, -ic)));
2208                        }
2209                    }
2210                    OpCode::ShlI => {
2211                        let ra = base + i.arg_a();
2212                        let v = state.get_at(base + i.arg_b());
2213                        let ic = i.arg_s_c() as i64;
2214                        if let Some(ib) = to_integer_ns(&v, F2Imod::Eq) {
2215                            pc += 1;
2216                            state.set_at(ra, LuaValue::Int(shiftl(ic, ib)));
2217                        }
2218                    }
2219                    // ── Arithmetic with register operands ──────────────────────
2220                    OpCode::Add => {
2221                        let ra = base + i.arg_a();
2222                        let rb = base + i.arg_b();
2223                        let rc = base + i.arg_c();
2224                        let ra_u = ra.0 as usize;
2225                        let rb_v = state.stack[rb.0 as usize].val;
2226                        let rc_v = state.stack[rc.0 as usize].val;
2227                        if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (rb_v, rc_v) {
2228                            pc += 1;
2229                            state.stack[ra_u].val = LuaValue::Int(intop_add(i1, i2));
2230                        } else if let (Some(n1), Some(n2)) = (number_value(rb_v), number_value(rc_v)) {
2231                            pc += 1;
2232                            state.stack[ra_u].val = LuaValue::Float(n1 + n2);
2233                        }
2234                    }
2235                    OpCode::Sub => {
2236                        let ra = base + i.arg_a();
2237                        let rb = base + i.arg_b();
2238                        let rc = base + i.arg_c();
2239                        let ra_u = ra.0 as usize;
2240                        let rb_v = state.stack[rb.0 as usize].val;
2241                        let rc_v = state.stack[rc.0 as usize].val;
2242                        if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (rb_v, rc_v) {
2243                            pc += 1;
2244                            state.stack[ra_u].val = LuaValue::Int(intop_sub(i1, i2));
2245                        } else if let (Some(n1), Some(n2)) = (number_value(rb_v), number_value(rc_v)) {
2246                            pc += 1;
2247                            state.stack[ra_u].val = LuaValue::Float(n1 - n2);
2248                        }
2249                    }
2250                    OpCode::Mul => {
2251                        let ra = base + i.arg_a();
2252                        let rb = base + i.arg_b();
2253                        let rc = base + i.arg_c();
2254                        if let Some((i1, i2)) = state.get_int_pair_at(rb, rc) {
2255                            pc += 1;
2256                            state.set_at(ra, LuaValue::Int(intop_mul(i1, i2)));
2257                        } else if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
2258                            pc += 1;
2259                            state.set_at(ra, LuaValue::Float(n1 * n2));
2260                        }
2261                    }
2262                    OpCode::Mod => {
2263                        let ra = base + i.arg_a();
2264                        let v1 = state.get_at(base + i.arg_b());
2265                        let v2 = state.get_at(base + i.arg_c());
2266                        state.set_ci_savedpc(ci, pc);
2267                        state.set_top(state.ci_top(ci));
2268                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
2269                            |a, b| imod(a, b), fmodf)?;
2270                    }
2271                    OpCode::Pow => {
2272                        let ra = base + i.arg_a();
2273                        let rb = base + i.arg_b();
2274                        let rc = base + i.arg_c();
2275                        if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
2276                            pc += 1;
2277                            let r = if n2 == 2.0 { n1 * n1 } else { n1.powf(n2) };
2278                            state.set_at(ra, LuaValue::Float(r));
2279                        }
2280                    }
2281                    OpCode::Div => {
2282                        let ra = base + i.arg_a();
2283                        let rb = base + i.arg_b();
2284                        let rc = base + i.arg_c();
2285                        if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
2286                            pc += 1;
2287                            state.set_at(ra, LuaValue::Float(n1 / n2));
2288                        }
2289                    }
2290                    OpCode::IDiv => {
2291                        let ra = base + i.arg_a();
2292                        let v1 = state.get_at(base + i.arg_b());
2293                        let v2 = state.get_at(base + i.arg_c());
2294                        state.set_ci_savedpc(ci, pc);
2295                        state.set_top(state.ci_top(ci));
2296                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
2297                            |a, b| idiv(a, b), |a, b| (a / b).floor())?;
2298                    }
2299                    // ── Bitwise with register operands ─────────────────────────
2300                    // if (tointegerns(v1, &i1) && tointegerns(v2, &i2)) { pc++; setivalue... }
2301                    OpCode::BAnd => {
2302                        let ra = base + i.arg_a();
2303                        let v1 = state.get_at(base + i.arg_b());
2304                        let v2 = state.get_at(base + i.arg_c());
2305                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_band);
2306                    }
2307                    OpCode::BOr => {
2308                        let ra = base + i.arg_a();
2309                        let v1 = state.get_at(base + i.arg_b());
2310                        let v2 = state.get_at(base + i.arg_c());
2311                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_bor);
2312                    }
2313                    OpCode::BXOr => {
2314                        let ra = base + i.arg_a();
2315                        let v1 = state.get_at(base + i.arg_b());
2316                        let v2 = state.get_at(base + i.arg_c());
2317                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_bxor);
2318                    }
2319                    OpCode::Shr => {
2320                        let ra = base + i.arg_a();
2321                        let v1 = state.get_at(base + i.arg_b());
2322                        let v2 = state.get_at(base + i.arg_c());
2323                        bitwise_shift_rr(state, ra, &v1, &v2, &mut pc, true);
2324                    }
2325                    OpCode::Shl => {
2326                        let ra = base + i.arg_a();
2327                        let v1 = state.get_at(base + i.arg_b());
2328                        let v2 = state.get_at(base + i.arg_c());
2329                        bitwise_shift_rr(state, ra, &v1, &v2, &mut pc, false);
2330                    }
2331                    // ── OP_MMBIN ─────────────────────────────────────────────
2332                    // Instruction pi = *(pc - 2); TMS tm = (TMS)GETARG_C(i);
2333                    // StkId result = RA(pi);
2334                    // Protect(luaT_trybinTM(L, s2v(ra), rb, result, tm));
2335                    OpCode::MmBin => {
2336                        let ra_idx = base + i.arg_a();
2337                        let rb_idx = base + i.arg_b();
2338                        let ra_v = state.get_at(ra_idx);
2339                        let rb_v = state.get_at(rb_idx);
2340                        let tm = tagmethod_from_index(i.arg_c() as usize);
2341                        let prev_inst = code[(pc - 2) as usize];
2342                        let result_idx = base + prev_inst.arg_a();
2343                        state.set_ci_savedpc(ci, pc);
2344                        state.set_top(state.ci_top(ci));
2345                        state.try_bin_tm(&ra_v, Some(ra_idx), &rb_v, Some(rb_idx), result_idx, tm)?;
2346                        trap = state.ci_trap(ci);
2347                    }
2348                    OpCode::MmBinI => {
2349                        let ra_idx = base + i.arg_a();
2350                        let ra_v = state.get_at(ra_idx);
2351                        let imm = i.arg_s_b() as i64;
2352                        let tm = tagmethod_from_index(i.arg_c() as usize);
2353                        let flip = i.arg_k() != 0;
2354                        let prev_inst = code[(pc - 2) as usize];
2355                        let result_idx = base + prev_inst.arg_a();
2356                        state.set_ci_savedpc(ci, pc);
2357                        state.set_top(state.ci_top(ci));
2358                        state.try_bin_i_tm(&ra_v, Some(ra_idx), imm, flip, result_idx, tm)?;
2359                        trap = state.ci_trap(ci);
2360                    }
2361                    OpCode::MmBinK => {
2362                        let ra_idx = base + i.arg_a();
2363                        let ra_v = state.get_at(ra_idx);
2364                        let imm = constants[i.arg_b() as usize];
2365                        let tm = tagmethod_from_index(i.arg_c() as usize);
2366                        let flip = i.arg_k() != 0;
2367                        let prev_inst = code[(pc - 2) as usize];
2368                        let result_idx = base + prev_inst.arg_a();
2369                        state.set_ci_savedpc(ci, pc);
2370                        state.set_top(state.ci_top(ci));
2371                        state.try_bin_assoc_tm(&ra_v, Some(ra_idx), &imm, None, flip, result_idx, tm)?;
2372                        trap = state.ci_trap(ci);
2373                    }
2374                    // ── OP_UNM ───────────────────────────────────────────────
2375                    //    else if (tonumberns(rb, nb)) setfltvalue(s2v(ra), -nb)
2376                    //    else Protect(luaT_trybinTM(L, rb, rb, ra, TM_UNM))
2377                    OpCode::Unm => {
2378                        let ra = base + i.arg_a();
2379                        let rb_idx = base + i.arg_b();
2380                        let rb_v = state.get_at(rb_idx);
2381                        match &rb_v {
2382                            LuaValue::Int(ib) => {
2383                                state.set_at(ra, LuaValue::Int(intop_sub(0, *ib)));
2384                            }
2385                            LuaValue::Float(nb) => {
2386                                state.set_at(ra, LuaValue::Float(-nb));
2387                            }
2388                            _ => {
2389                                state.set_ci_savedpc(ci, pc);
2390                                state.set_top(state.ci_top(ci));
2391                                state.try_bin_tm(&rb_v, Some(rb_idx), &rb_v, Some(rb_idx), ra, TagMethod::Unm)?;
2392                                trap = state.ci_trap(ci);
2393                            }
2394                        }
2395                    }
2396                    // ── OP_BNOT ──────────────────────────────────────────────
2397                    OpCode::BNot => {
2398                        let ra = base + i.arg_a();
2399                        let rb_idx = base + i.arg_b();
2400                        let rb_v = state.get_at(rb_idx);
2401                        if let Some(ib) = to_integer_ns(&rb_v, F2Imod::Eq) {
2402                            state.set_at(ra, LuaValue::Int(!ib));
2403                        } else {
2404                            state.set_ci_savedpc(ci, pc);
2405                            state.set_top(state.ci_top(ci));
2406                            state.try_bin_tm(&rb_v, Some(rb_idx), &rb_v, Some(rb_idx), ra, TagMethod::Bnot)?;
2407                            trap = state.ci_trap(ci);
2408                        }
2409                    }
2410                    // ── OP_NOT ───────────────────────────────────────────────
2411                    OpCode::Not => {
2412                        let ra = base + i.arg_a();
2413                        let rb_v = state.get_at(base + i.arg_b());
2414                        let falsy = matches!(rb_v, LuaValue::Nil | LuaValue::Bool(false));
2415                        state.set_at(ra, LuaValue::Bool(falsy));
2416                    }
2417                    // ── OP_LEN ───────────────────────────────────────────────
2418                    OpCode::Len => {
2419                        let ra = base + i.arg_a();
2420                        let rb_idx = base + i.arg_b();
2421                        let rb_v = state.get_at(rb_idx);
2422                        state.set_ci_savedpc(ci, pc);
2423                        state.set_top(state.ci_top(ci));
2424                        obj_len(state, ra, rb_v, rb_idx)?;
2425                        trap = state.ci_trap(ci);
2426                    }
2427                    // ── OP_CONCAT ─────────────────────────────────────────────
2428                    OpCode::Concat => {
2429                        let ra = base + i.arg_a();
2430                        let n = i.arg_b() as i32;
2431                        state.set_top(ra + n as i32);
2432                        state.set_ci_savedpc(ci, pc); // ProtectNT: save pc only
2433                        concat(state, n)?;
2434                        let top = state.top_idx();
2435                        state.set_ci_savedpc(ci, pc);
2436                        state.set_top(top);
2437                        state.gc_cond_step();
2438                        trap = state.ci_trap(ci);
2439                    }
2440                    // ── OP_CLOSE ──────────────────────────────────────────────
2441                    OpCode::Close => {
2442                        let ra = base + i.arg_a();
2443                        state.set_ci_savedpc(ci, pc);
2444                        state.set_top(state.ci_top(ci));
2445                        crate::func::close(state, ra, lua_types::status::LuaStatus::Ok as i32, true)?;
2446                        trap = state.ci_trap(ci);
2447                    }
2448                    // ── OP_TBC ────────────────────────────────────────────────
2449                    OpCode::Tbc => {
2450                        let ra = base + i.arg_a();
2451                        state.set_ci_savedpc(ci, pc);
2452                        state.set_top(state.ci_top(ci));
2453                        state.new_tbc_upval(ra)?;
2454                    }
2455                    // ── OP_JMP ────────────────────────────────────────────────
2456                    OpCode::Jmp => {
2457                        pc = (pc as i64 + i.arg_s_j() as i64) as u32;
2458                        trap = state.ci_trap(ci);
2459                    }
2460                    // ── OP_EQ ─────────────────────────────────────────────────
2461                    OpCode::Eq => {
2462                        let ra_v = state.get_at(base + i.arg_a());
2463                        let rb_v = state.get_at(base + i.arg_b());
2464                        state.set_ci_savedpc(ci, pc);
2465                        state.set_top(state.ci_top(ci));
2466                        let cond = equal_obj(Some(state), &ra_v, &rb_v)? as u32;
2467                        trap = state.ci_trap(ci);
2468                        if (cond as i32) != i.arg_k() {
2469                            pc += 1;
2470                        } else {
2471                            let next = code[pc as usize];
2472                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2473                            trap = state.ci_trap(ci);
2474                        }
2475                    }
2476                    // ── OP_LT ─────────────────────────────────────────────────
2477                    OpCode::Lt => {
2478                        let ra_v = state.get_at(base + i.arg_a());
2479                        let rb_v = state.get_at(base + i.arg_b());
2480                        let cond = if let (LuaValue::Int(ia), LuaValue::Int(ib)) = (&ra_v, &rb_v) {
2481                            *ia < *ib
2482                        } else if matches!((&ra_v, &rb_v),
2483                            (LuaValue::Int(_) | LuaValue::Float(_),
2484                             LuaValue::Int(_) | LuaValue::Float(_))) {
2485                            lt_num(&ra_v, &rb_v)
2486                        } else {
2487                            state.set_ci_savedpc(ci, pc);
2488                            state.set_top(state.ci_top(ci));
2489                            let r = less_than_others(state, &ra_v, &rb_v)?;
2490                            trap = state.ci_trap(ci);
2491                            r
2492                        };
2493                        if (cond as i32) != i.arg_k() {
2494                            pc += 1;
2495                        } else {
2496                            let next = code[pc as usize];
2497                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2498                            trap = state.ci_trap(ci);
2499                        }
2500                    }
2501                    // ── OP_LE ─────────────────────────────────────────────────
2502                    OpCode::Le => {
2503                        let ra_v = state.get_at(base + i.arg_a());
2504                        let rb_v = state.get_at(base + i.arg_b());
2505                        let cond = if let (LuaValue::Int(ia), LuaValue::Int(ib)) = (&ra_v, &rb_v) {
2506                            *ia <= *ib
2507                        } else if matches!((&ra_v, &rb_v),
2508                            (LuaValue::Int(_) | LuaValue::Float(_),
2509                             LuaValue::Int(_) | LuaValue::Float(_))) {
2510                            le_num(&ra_v, &rb_v)
2511                        } else {
2512                            state.set_ci_savedpc(ci, pc);
2513                            state.set_top(state.ci_top(ci));
2514                            let r = less_equal_others(state, &ra_v, &rb_v)?;
2515                            trap = state.ci_trap(ci);
2516                            r
2517                        };
2518                        if (cond as i32) != i.arg_k() {
2519                            pc += 1;
2520                        } else {
2521                            let next = code[pc as usize];
2522                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2523                            trap = state.ci_trap(ci);
2524                        }
2525                    }
2526                    // ── OP_EQK ────────────────────────────────────────────────
2527                    OpCode::EqK => {
2528                        let ra_v = state.get_at(base + i.arg_a());
2529                        let rb_v = constants[i.arg_b() as usize];
2530                        let cond = equal_obj(None, &ra_v, &rb_v)? as u32;
2531                        if (cond as i32) != i.arg_k() {
2532                            pc += 1;
2533                        } else {
2534                            let next = code[pc as usize];
2535                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2536                            trap = state.ci_trap(ci);
2537                        }
2538                    }
2539                    // ── OP_EQI ────────────────────────────────────────────────
2540                    //    if (ttisinteger) cond = ivalue == im
2541                    //    elif (ttisfloat) cond = numeq(fltvalue, cast_num(im))
2542                    //    else cond = 0
2543                    OpCode::EqI => {
2544                        let ra_v = state.get_at(base + i.arg_a());
2545                        let im = i.arg_s_b() as i64;
2546                        let cond: bool = match &ra_v {
2547                            LuaValue::Int(iv) => *iv == im,
2548                            LuaValue::Float(fv) => *fv == im as f64,
2549                            _ => false,
2550                        };
2551                        if (cond as i32) != i.arg_k() {
2552                            pc += 1;
2553                        } else {
2554                            let next = code[pc as usize];
2555                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2556                            trap = state.ci_trap(ci);
2557                        }
2558                    }
2559                    // ── OP_LTI / OP_LEI / OP_GTI / OP_GEI ───────────────────
2560                    //              inv=0/0/1/1, tm=TM_LT/TM_LE/TM_LT/TM_LE)
2561                    OpCode::LtI => {
2562                        let ra = base + i.arg_a();
2563                        let im = i.arg_s_b() as i64;
2564                        let fast_cond = match &state.stack[ra.0 as usize].val {
2565                            LuaValue::Int(ia) => Some(*ia < im),
2566                            LuaValue::Float(fa) => Some(*fa < im as f64),
2567                            _ => None,
2568                        };
2569                        let cond = match fast_cond {
2570                            Some(cond) => cond,
2571                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, false, TagMethod::Lt)?,
2572                        };
2573                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2574                    }
2575                    OpCode::LeI => {
2576                        let ra = base + i.arg_a();
2577                        let im = i.arg_s_b() as i64;
2578                        let fast_cond = match &state.stack[ra.0 as usize].val {
2579                            LuaValue::Int(ia) => Some(*ia <= im),
2580                            LuaValue::Float(fa) => Some(*fa <= im as f64),
2581                            _ => None,
2582                        };
2583                        let cond = match fast_cond {
2584                            Some(cond) => cond,
2585                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, false, TagMethod::Le)?,
2586                        };
2587                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2588                    }
2589                    OpCode::GtI => {
2590                        let ra = base + i.arg_a();
2591                        let im = i.arg_s_b() as i64;
2592                        let fast_cond = match &state.stack[ra.0 as usize].val {
2593                            LuaValue::Int(ia) => Some(*ia > im),
2594                            LuaValue::Float(fa) => Some(*fa > im as f64),
2595                            _ => None,
2596                        };
2597                        let cond = match fast_cond {
2598                            Some(cond) => cond,
2599                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, true, TagMethod::Lt)?,
2600                        };
2601                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2602                    }
2603                    OpCode::GeI => {
2604                        let ra = base + i.arg_a();
2605                        let im = i.arg_s_b() as i64;
2606                        let fast_cond = match &state.stack[ra.0 as usize].val {
2607                            LuaValue::Int(ia) => Some(*ia >= im),
2608                            LuaValue::Float(fa) => Some(*fa >= im as f64),
2609                            _ => None,
2610                        };
2611                        let cond = match fast_cond {
2612                            Some(cond) => cond,
2613                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, true, TagMethod::Le)?,
2614                        };
2615                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2616                    }
2617                    // ── OP_TEST ────────────────────────────────────────────────
2618                    OpCode::Test => {
2619                        let ra_v = state.get_at(base + i.arg_a());
2620                        let cond = !matches!(ra_v, LuaValue::Nil | LuaValue::Bool(false));
2621                        if (cond as i32) != i.arg_k() {
2622                            pc += 1;
2623                        } else {
2624                            let next = code[pc as usize];
2625                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2626                            trap = state.ci_trap(ci);
2627                        }
2628                    }
2629                    // ── OP_TESTSET ─────────────────────────────────────────────
2630                    //    else { setobj2s(L, ra, rb); donextjump(ci); }
2631                    OpCode::TestSet => {
2632                        let ra = base + i.arg_a();
2633                        let rb_v = state.get_at(base + i.arg_b());
2634                        let falsy = matches!(rb_v, LuaValue::Nil | LuaValue::Bool(false));
2635                        if (falsy as i32) == i.arg_k() {
2636                            pc += 1;
2637                        } else {
2638                            state.set_at(ra, rb_v);
2639                            let next = code[pc as usize];
2640                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2641                            trap = state.ci_trap(ci);
2642                        }
2643                    }
2644                    // ── OP_CALL ────────────────────────────────────────────────
2645                    //      updatetrap(ci);
2646                    //    else { ci = newci; goto startfunc; }
2647                    OpCode::Call => {
2648                        let ra = base + i.arg_a();
2649                        let b = i.arg_b();
2650                        let nresults = i.arg_c() as i32 - 1;
2651                        if b != 0 {
2652                            state.set_top(ra + b);
2653                        }
2654                        state.set_ci_savedpc(ci, pc); // savepc
2655                        let had_hook = state.hookmask != 0;
2656                        match state.precall(ra, nresults)? {
2657                            None => {
2658                                // C functions such as debug.sethook can change
2659                                // hook state during the call, so refresh the VM
2660                                // trap when hooks were or became relevant.
2661                                if had_hook || state.hookmask != 0 {
2662                                    trap = state.ci_trap(ci); // updatetrap
2663                                }
2664                            }
2665                            Some(new_ci) => {
2666                                // Lua function — goto startfunc
2667                                ci = new_ci;
2668                                continue 'startfunc;
2669                            }
2670                        }
2671                    }
2672                    // ── OP_TAILCALL ────────────────────────────────────────────
2673                    //      goto startfunc;
2674                    //    else { ci->func.p -= delta; luaD_poscall(L, ci, n);
2675                    //            updatetrap; goto ret; }
2676                    OpCode::TailCall => {
2677                        let ra = base + i.arg_a();
2678                        let b = i.arg_b();
2679                        let nparams1 = i.arg_c();
2680                        let delta = if nparams1 != 0 {
2681                            state.ci_nextraargs(ci) + nparams1 as i32
2682                        } else {
2683                            0
2684                        };
2685                        let top_b: i32 = if b != 0 {
2686                            state.set_top(ra + b);
2687                            b
2688                        } else {
2689                            state.top_idx() - ra
2690                        };
2691                        state.set_ci_savedpc(ci, pc);
2692                        if i.test_k() {
2693                            state.close_upvals_from_base(ci)?;
2694                        }
2695                        let n = state.pretailcall(ci, ra, top_b, delta)?;
2696                        if n < 0 {
2697                            // Lua function — goto startfunc
2698                            continue 'startfunc;
2699                        } else {
2700                            // C function — ci->func.p -= delta; luaD_poscall; goto ret
2701                            state.ci_adjust_func(ci, delta);
2702                            state.poscall(ci, n as u32)?;
2703                            if state.hookmask != 0 {
2704                                trap = state.ci_trap(ci);
2705                            }
2706                            break 'dispatch; // goto ret
2707                        }
2708                    }
2709                    // ── OP_RETURN ──────────────────────────────────────────────
2710                    //    savepc; if TESTARG_k: close upvals;
2711                    //    if nparams1: ci->func -= nextraargs+nparams1;
2712                    //    L->top.p = ra+n; luaD_poscall; goto ret
2713                    OpCode::Return => {
2714                        let ra = base + i.arg_a();
2715                        let n_raw = i.arg_b() as i32 - 1;
2716                        let nparams1 = i.arg_c();
2717                        let n: u32 = if n_raw < 0 {
2718                            (state.top_idx() - ra) as u32
2719                        } else {
2720                            n_raw as u32
2721                        };
2722                        state.set_ci_savedpc(ci, pc);
2723                        if i.test_k() {
2724                            state.ci_nres_set(ci, n as i32);
2725                            let ci_top = state.ci_top(ci);
2726                            if state.top_idx().0 < ci_top.0 {
2727                                state.set_top(ci_top);
2728                            }
2729                            crate::func::close(state, base, crate::func::CLOSE_K_TOP, true)?;
2730                            if state.hookmask != 0 {
2731                                trap = state.ci_trap(ci);
2732                            }
2733                            base = state.ci_base(ci); // updatestack
2734                        }
2735                        if nparams1 != 0 {
2736                            let nextraargs = state.ci_nextraargs(ci) as u32;
2737                            state.ci_adjust_func(ci, nextraargs as i32 + nparams1 as i32);
2738                        }
2739                        state.set_top(ra + n as i32);
2740                        state.poscall(ci, n)?;
2741                        if state.hookmask != 0 {
2742                            trap = state.ci_trap(ci);
2743                        }
2744                        break 'dispatch; // goto ret
2745                    }
2746                    // ── OP_RETURN0 ─────────────────────────────────────────────
2747                    //    else { L->ci = ci->previous; L->top = base-1;
2748                    //           for (nres = ci->nresults; nres > 0; nres--)
2749                    //             setnilvalue(L->top++) }
2750                    //    goto ret;
2751                    OpCode::Return0 => {
2752                        if state.hookmask == 0 {
2753                            let ci_slot = ci.as_usize();
2754                            let nres = state.call_info[ci_slot].nresults as i32;
2755                            state.ci = state.call_info[ci_slot]
2756                                .previous
2757                                .expect("RETURN0: returning frame has no previous CallInfo");
2758                            state.top = base - 1;
2759                            for _ in 0..nres.max(0) {
2760                                state.push(LuaValue::Nil);
2761                            }
2762                        } else {
2763                            return0_hook(state, ci, base, i, pc, &mut trap)?;
2764                        }
2765                        break 'dispatch; // goto ret
2766                    }
2767                    // ── OP_RETURN1 ─────────────────────────────────────────────
2768                    //    else { nres = ci->nresults; ci = ci->previous; ...handle results... }
2769                    //    goto ret;
2770                    OpCode::Return1 => {
2771                        if state.hookmask == 0 {
2772                            let ci_slot = ci.as_usize();
2773                            let nres = state.call_info[ci_slot].nresults as i32;
2774                            state.ci = state.call_info[ci_slot]
2775                                .previous
2776                                .expect("RETURN1: returning frame has no previous CallInfo");
2777                            if nres == 0 {
2778                                state.top = base - 1;
2779                            } else {
2780                                let ra = base + i.arg_a();
2781                                state.stack[(base - 1).0 as usize].val =
2782                                    state.stack[ra.0 as usize].val; // at least this result
2783                                state.top = base;
2784                                for _ in 1..nres.max(0) {
2785                                    state.push(LuaValue::Nil);
2786                                }
2787                            }
2788                        } else {
2789                            return1_hook(state, ci, base, i, pc, &mut trap)?;
2790                        }
2791                        break 'dispatch; // goto ret
2792                    }
2793                    // ── OP_FORLOOP ─────────────────────────────────────────────
2794                    //    else if (floatforloop(ra)) pc -= GETARG_Bx(i)
2795                    //    updatetrap(ci);
2796                    OpCode::ForLoop => {
2797                        let ra = base + i.arg_a();
2798                        if legacy_for {
2799                            if forloop_legacy(state, ra) {
2800                                pc = (pc as i64 - i.arg_bx() as i64) as u32;
2801                            }
2802                            trap = state.ci_trap(ci);
2803                        } else {
2804                            let ra_u = ra.0 as usize;
2805                            if let LuaValue::Int(step) = state.stack[ra_u + 2].val {
2806                                let count = match state.stack[ra_u + 1].val {
2807                                    LuaValue::Int(c) => c as u64,
2808                                    _ => 0,
2809                                };
2810                                if count > 0 {
2811                                    let idx = match state.stack[ra_u].val {
2812                                        LuaValue::Int(x) => x,
2813                                        _ => 0,
2814                                    };
2815                                    state.stack[ra_u + 1].val = LuaValue::Int((count - 1) as i64);
2816                                    let new_idx = intop_add(idx, step);
2817                                    state.stack[ra_u].val = LuaValue::Int(new_idx);
2818                                    state.stack[ra_u + 3].val = LuaValue::Int(new_idx);
2819                                    pc = (pc as i64 - i.arg_bx() as i64) as u32;
2820                                }
2821                            } else if float_for_loop(state, ra) {
2822                                pc = (pc as i64 - i.arg_bx() as i64) as u32;
2823                            }
2824                            trap = state.ci_trap(ci);
2825                        }
2826                    }
2827                    // ── OP_FORPREP ─────────────────────────────────────────────
2828                    OpCode::ForPrep => {
2829                        let ra = base + i.arg_a();
2830                        state.set_ci_savedpc(ci, pc);
2831                        state.set_top(state.ci_top(ci));
2832                        if legacy_for {
2833                            // 5.3: prep subtracts the step and ALWAYS jumps forward
2834                            // to FORLOOP (which runs the first test).
2835                            forprep_legacy(state, ra)?;
2836                            pc = (pc as i64 + i.arg_bx() as i64) as u32;
2837                        } else if forprep(state, ra)? {
2838                            pc = (pc as i64 + i.arg_bx() as i64 + 1) as u32;
2839                        }
2840                    }
2841                    // ── OP_TFORPREP ────────────────────────────────────────────
2842                    //    pc += GETARG_Bx(i); i = *pc++; assert(OP_TFORCALL && ra==RA(i));
2843                    //    goto l_tforcall;
2844                    OpCode::TForPrep => {
2845                        let ra = base + i.arg_a();
2846                        state.set_ci_savedpc(ci, pc);
2847                        state.set_top(state.ci_top(ci));
2848                        state.new_tbc_upval(ra + 3)?;
2849                        pc = (pc as i64 + i.arg_bx() as i64) as u32;
2850                        let tfc_i = code[pc as usize];
2851                        pc += 1;
2852                        debug_assert!(tfc_i.opcode() == OpCode::TForCall);
2853                        // inline l_tforcall:
2854                        let tfc_ra = base + tfc_i.arg_a();
2855                        for k in 0..3u32 {
2856                            let v = state.get_at(tfc_ra + k as i32);
2857                            state.set_at(tfc_ra + 4 + k as i32, v);
2858                        }
2859                        state.set_top(tfc_ra + 4 + 3);
2860                        state.set_ci_savedpc(ci, pc);
2861                        state.call_at(tfc_ra + 4, tfc_i.arg_c() as i32)?;
2862                        trap = state.ci_trap(ci);
2863                        base = state.ci_base(ci); // updatestack
2864                        let tfl_i = code[pc as usize];
2865                        pc += 1;
2866                        debug_assert!(tfl_i.opcode() == OpCode::TForLoop);
2867                        let tfl_ra = base + tfl_i.arg_a();
2868                        // inline l_tforloop:
2869                        if !matches!(state.get_at(tfl_ra + 4), LuaValue::Nil) {
2870                            let v = state.get_at(tfl_ra + 4);
2871                            state.set_at(tfl_ra + 2, v);
2872                            pc = (pc as i64 - tfl_i.arg_bx() as i64) as u32;
2873                        }
2874                    }
2875                    // ── OP_TFORCALL ────────────────────────────────────────────
2876                    OpCode::TForCall => {
2877                        let ra = base + i.arg_a();
2878                        for k in 0..3u32 {
2879                            let v = state.get_at(ra + k as i32);
2880                            state.set_at(ra + 4 + k as i32, v);
2881                        }
2882                        state.set_top(ra + 4 + 3);
2883                        state.set_ci_savedpc(ci, pc);
2884                        state.call_at(ra + 4, i.arg_c() as i32)?;
2885                        trap = state.ci_trap(ci);
2886                        base = state.ci_base(ci); // updatestack
2887                        let tfl_i = code[pc as usize];
2888                        pc += 1;
2889                        debug_assert!(tfl_i.opcode() == OpCode::TForLoop);
2890                        let tfl_ra = base + tfl_i.arg_a();
2891                        if !matches!(state.get_at(tfl_ra + 4), LuaValue::Nil) {
2892                            let v = state.get_at(tfl_ra + 4);
2893                            state.set_at(tfl_ra + 2, v);
2894                            pc = (pc as i64 - tfl_i.arg_bx() as i64) as u32;
2895                        }
2896                    }
2897                    // ── OP_TFORLOOP ────────────────────────────────────────────
2898                    OpCode::TForLoop => {
2899                        let ra = base + i.arg_a();
2900                        if !matches!(state.get_at(ra + 4), LuaValue::Nil) {
2901                            let v = state.get_at(ra + 4);
2902                            state.set_at(ra + 2, v);
2903                            pc = (pc as i64 - i.arg_bx() as i64) as u32;
2904                        }
2905                    }
2906                    // ── OP_SETLIST ─────────────────────────────────────────────
2907                    //    if TESTARG_k: last += Ax * (MAXARG_C+1); pc++;
2908                    //    for (; n > 0; n--) h->array[last-1] = val; luaC_barrierback
2909                    OpCode::SetList => {
2910                        let ra = base + i.arg_a();
2911                        let n_raw = i.arg_b();
2912                        let mut last = i.arg_c();
2913                        let t_val = state.get_at(ra);
2914                        let n: i32 = if n_raw == 0 {
2915                            state.top_idx() - ra - 1
2916                        } else {
2917                            state.set_top(state.ci_top(ci));
2918                            n_raw
2919                        };
2920                        last += n;
2921                        if i.test_k() {
2922                            let extra = code[pc as usize];
2923                            pc += 1;
2924                            const MAXARG_C: i32 = (1 << 8) - 1;
2925                            last += extra.arg_ax() * (MAXARG_C + 1);
2926                        }
2927                        state.table_ensure_array(&t_val, last as usize)?;
2928                        for k in (1..=n).rev() {
2929                            let val = state.get_at(ra + k as i32);
2930                            state.table_array_set(&t_val, (last - 1) as usize, val.clone())?;
2931                            last -= 1;
2932                            state.gc_value_barrier_back(&t_val, &val);
2933                        }
2934                    }
2935                    // ── OP_CLOSURE ─────────────────────────────────────────────
2936                    //    halfProtect(pushclosure(L, p, cl->upvals, base, ra));
2937                    //    checkGC(L, ra+1);
2938                    OpCode::Closure => {
2939                        let ra = base + i.arg_a();
2940                        let proto_idx = i.arg_bx() as usize;
2941                        state.set_ci_savedpc(ci, pc);
2942                        state.set_top(state.ci_top(ci));
2943                        push_closure(state, proto_idx, ci, base, ra)?;
2944                        // checkGC
2945                        state.set_ci_savedpc(ci, pc);
2946                        state.set_top(ra + 1);
2947                        state.gc_cond_step();
2948                        trap = state.ci_trap(ci);
2949                    }
2950                    // ── OP_VARARG ──────────────────────────────────────────────
2951                    OpCode::VarArg => {
2952                        let ra = base + i.arg_a();
2953                        let n = i.arg_c() as i32 - 1;
2954                        state.set_ci_savedpc(ci, pc);
2955                        state.set_top(state.ci_top(ci));
2956                        state.get_varargs(ci, ra, n)?;
2957                        trap = state.ci_trap(ci);
2958                    }
2959                    // ── OP_VARARGPREP ──────────────────────────────────────────
2960                    //    if (trap) luaD_hookcall(L, ci); L->oldpc = 1;
2961                    //    updatebase(ci);
2962                    OpCode::VarArgPrep => {
2963                        let nparams = i.arg_a();
2964                        state.set_ci_savedpc(ci, pc);
2965                        state.adjust_varargs(ci, nparams, &cl)?;
2966                        trap = state.ci_trap(ci);
2967                        if trap {
2968                            state.hook_call(ci)?;
2969                            state.set_oldpc(1);
2970                        }
2971                        base = state.ci_base(ci);
2972                    }
2973                    // ── OP_EXTRAARG ────────────────────────────────────────────
2974                    OpCode::ExtraArg => {
2975                        debug_assert!(false, "OP_EXTRAARG executed directly");
2976                    }
2977                    // ── OP_ERRNNIL (Lua 5.5 global-already-defined guard) ──────
2978                    //    luaG_errnnil: if the global's current value is non-nil,
2979                    //    raise `global '<name>' already defined`. Bx == 0 → "?",
2980                    //    else Bx-1 indexes the constant table for the name.
2981                    OpCode::ErrNNil => {
2982                        let ra = base + i.arg_a();
2983                        if !matches!(state.get_at(ra), LuaValue::Nil) {
2984                            let bx = i.arg_bx();
2985                            let name: Vec<u8> = if bx == 0 {
2986                                b"?".to_vec()
2987                            } else {
2988                                match constants[(bx - 1) as usize] {
2989                                    LuaValue::Str(s) => s.as_bytes().to_vec(),
2990                                    _ => b"?".to_vec(),
2991                                }
2992                            };
2993                            let mut msg = Vec::with_capacity(name.len() + 24);
2994                            msg.extend_from_slice(b"global '");
2995                            msg.extend_from_slice(&name);
2996                            msg.extend_from_slice(b"' already defined");
2997                            state.set_ci_savedpc(ci, pc);
2998                            return Err(crate::debug::prefixed_runtime_pub(state, msg));
2999                        }
3000                    }
3001                    // ── OP_VARARGPACK (Lua 5.5 named varargs) ──────────────────
3002                    //    Pack the current frame's extra varargs into a fresh
3003                    //    table stored in register A. Mirrors `table.pack(...)`:
3004                    //    a 1-based sequence of all extra args plus an integer
3005                    //    `.n` field counting them (nil holes included). The
3006                    //    extra args were moved by VARARGPREP to the slots just
3007                    //    below `ci->func`, i.e. `ci_func - nextra .. ci_func-1`.
3008                    OpCode::VarArgPack => {
3009                        let ra = base + i.arg_a();
3010                        let nextra = state.ci_nextraargs(ci);
3011                        let ci_func: StackIdx = state.ci_base(ci) - 1;
3012                        let t = if nextra > 0 {
3013                            state.new_table_with_sizes(nextra as u32, 1)?
3014                        } else {
3015                            state.new_table()
3016                        };
3017                        for k in 0..nextra {
3018                            let src: StackIdx = ci_func - nextra as i32 + k as i32;
3019                            let val = state.get_at(src);
3020                            t.raw_set_int(state, (k + 1) as i64, val)?;
3021                        }
3022                        let n_key = state.intern_str(b"n")?;
3023                        t.raw_set(state, LuaValue::Str(n_key), LuaValue::Int(nextra as i64))?;
3024                        state.set_at(ra, LuaValue::Table(t));
3025                        state.set_ci_savedpc(ci, pc);
3026                        state.gc_cond_step();
3027                        if state.hookmask != 0 {
3028                            trap = state.ci_trap(ci);
3029                        }
3030                    }
3031                } // end match opcode
3032            } // end 'dispatch loop
3033
3034            // ── ret: label ──────────────────────────────────────────────────
3035            if state.ci_is_fresh(ci) {
3036                return Ok(());
3037            } else {
3038                ci = state.ci_previous(ci).expect("ci_previous: not fresh frame must have previous");
3039                continue 'returning;
3040            }
3041        } // end 'returning loop
3042    } // end 'startfunc loop
3043}
3044
3045// ─── Local opcode dispatch helpers ───────────────────────────────────────────
3046
3047#[inline(always)]
3048fn number_value(v: LuaValue) -> Option<f64> {
3049    match v {
3050        LuaValue::Float(f) => Some(f),
3051        LuaValue::Int(i) => Some(i as f64),
3052        _ => None,
3053    }
3054}
3055
3056/// Increments `pc` on success (the `pc++` in the C macros).
3057#[allow(dead_code)]
3058#[inline]
3059fn arith_op_aux_rr(
3060    state: &mut LuaState,
3061    ra: StackIdx,
3062    v1: &LuaValue,
3063    v2: &LuaValue,
3064    pc: &mut u32,
3065    iop: fn(i64, i64) -> i64,
3066    fop: fn(f64, f64) -> f64,
3067) {
3068    if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (v1, v2) {
3069        *pc += 1;
3070        state.set_at(ra, LuaValue::Int(iop(*i1, *i2)));
3071    } else {
3072        arith_float_aux(state, ra, v1, v2, pc, fop);
3073    }
3074}
3075
3076#[allow(dead_code)]
3077#[inline]
3078fn arith_float_aux(
3079    state: &mut LuaState,
3080    ra: StackIdx,
3081    v1: &LuaValue,
3082    v2: &LuaValue,
3083    pc: &mut u32,
3084    fop: fn(f64, f64) -> f64,
3085) {
3086    let n1 = match v1 {
3087        LuaValue::Float(f) => Some(*f),
3088        LuaValue::Int(i) => Some(*i as f64),
3089        _ => None,
3090    };
3091    let n2 = match v2 {
3092        LuaValue::Float(f) => Some(*f),
3093        LuaValue::Int(i) => Some(*i as f64),
3094        _ => None,
3095    };
3096    if let (Some(n1), Some(n2)) = (n1, n2) {
3097        *pc += 1;
3098        state.set_at(ra, LuaValue::Float(fop(n1, n2)));
3099    }
3100}
3101
3102#[allow(dead_code)]
3103#[inline]
3104fn arith_op_checked(
3105    state: &mut LuaState,
3106    ra: StackIdx,
3107    v1: &LuaValue,
3108    v2: &LuaValue,
3109    pc: &mut u32,
3110    iop: fn(i64, i64) -> Result<i64, LuaError>,
3111    fop: fn(f64, f64) -> f64,
3112) -> Result<(), LuaError> {
3113    if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (v1, v2) {
3114        *pc += 1;
3115        let result = iop(*i1, *i2).map_err(|e| match e {
3116            LuaError::Runtime(LuaValue::Str(s)) => {
3117                crate::debug::prefixed_runtime_pub(state, s.as_bytes().to_vec())
3118            }
3119            other => other,
3120        })?;
3121        state.set_at(ra, LuaValue::Int(result));
3122    } else {
3123        arith_float_aux(state, ra, v1, v2, pc, fop);
3124    }
3125    Ok(())
3126}
3127
3128#[allow(dead_code)]
3129#[inline]
3130fn bitwise_op_k(
3131    state: &mut LuaState,
3132    ra: StackIdx,
3133    v1: &LuaValue,
3134    v2: &LuaValue, // must be integer (K constant)
3135    pc: &mut u32,
3136    op: fn(i64, i64) -> i64,
3137) {
3138    let i2 = match v2 {
3139        LuaValue::Int(i) => *i,
3140        _ => return,
3141    };
3142    if let Some(i1) = to_integer_ns(v1, F2Imod::Eq) {
3143        *pc += 1;
3144        state.set_at(ra, LuaValue::Int(op(i1, i2)));
3145    }
3146}
3147
3148#[allow(dead_code)]
3149#[inline]
3150fn bitwise_op_rr(
3151    state: &mut LuaState,
3152    ra: StackIdx,
3153    v1: &LuaValue,
3154    v2: &LuaValue,
3155    pc: &mut u32,
3156    op: fn(i64, i64) -> i64,
3157) {
3158    if let (Some(i1), Some(i2)) = (
3159        to_integer_ns(v1, F2Imod::Eq),
3160        to_integer_ns(v2, F2Imod::Eq),
3161    ) {
3162        *pc += 1;
3163        state.set_at(ra, LuaValue::Int(op(i1, i2)));
3164    }
3165}
3166
3167/// `right = true` negates `y` for right-shift semantics.
3168#[allow(dead_code)]
3169#[inline]
3170fn bitwise_shift_rr(
3171    state: &mut LuaState,
3172    ra: StackIdx,
3173    v1: &LuaValue,
3174    v2: &LuaValue,
3175    pc: &mut u32,
3176    right: bool,
3177) {
3178    if let (Some(i1), Some(i2)) = (
3179        to_integer_ns(v1, F2Imod::Eq),
3180        to_integer_ns(v2, F2Imod::Eq),
3181    ) {
3182        let y = if right { intop_sub(0, i2) } else { i2 };
3183        *pc += 1;
3184        state.set_at(ra, LuaValue::Int(shiftl(i1, y)));
3185    }
3186}
3187
3188/// Cold half of C's `op_orderI` macro: only reached when the operand is not a
3189/// plain integer/float and a metamethod lookup may be needed.
3190#[cold]
3191#[inline(never)]
3192#[allow(clippy::too_many_arguments)]
3193fn order_imm_slow(
3194    state: &mut LuaState,
3195    ra: StackIdx,
3196    pc: u32,
3197    trap: &mut bool,
3198    ci: CallInfoIdx,
3199    i: Instruction,
3200    im: i64,
3201    inv: bool,
3202    tm: TagMethod,
3203) -> Result<bool, LuaError> {
3204    let ra_v = state.get_at(ra);
3205    let isf = i.arg_c() != 0;
3206    state.set_ci_savedpc(ci, pc);
3207    state.set_top(state.ci_top(ci));
3208    let r = state.call_order_i_tm(&ra_v, im, inv, isf, tm)?;
3209    *trap = state.ci_trap(ci);
3210    Ok(r)
3211}
3212
3213#[inline(always)]
3214fn finish_order_imm_jump(
3215    state: &mut LuaState,
3216    cl: &lua_types::GcRef<lua_types::LuaLClosure>,
3217    pc: &mut u32,
3218    trap: &mut bool,
3219    ci: CallInfoIdx,
3220    i: Instruction,
3221    cond: bool,
3222) {
3223    if (cond as i32) != i.arg_k() {
3224        *pc += 1;
3225    } else {
3226        let next = state.proto_code(&cl, *pc);
3227        *pc = (*pc as i64 + next.arg_s_j() as i64 + 1) as u32;
3228        *trap = state.ci_trap(ci);
3229    }
3230}
3231
3232#[cold]
3233#[inline(never)]
3234fn return0_hook(
3235    state: &mut LuaState,
3236    ci: CallInfoIdx,
3237    base: StackIdx,
3238    i: Instruction,
3239    pc: u32,
3240    trap: &mut bool,
3241) -> Result<(), LuaError> {
3242    let ra = base + i.arg_a();
3243    state.set_top(ra);
3244    state.set_ci_savedpc(ci, pc);
3245    state.poscall(ci, 0)?;
3246    *trap = true;
3247    Ok(())
3248}
3249
3250#[cold]
3251#[inline(never)]
3252fn return1_hook(
3253    state: &mut LuaState,
3254    ci: CallInfoIdx,
3255    base: StackIdx,
3256    i: Instruction,
3257    pc: u32,
3258    trap: &mut bool,
3259) -> Result<(), LuaError> {
3260    let ra = base + i.arg_a();
3261    state.set_top(ra + 1);
3262    state.set_ci_savedpc(ci, pc);
3263    state.poscall(ci, 1)?;
3264    *trap = true;
3265    Ok(())
3266}
3267
3268// ──────────────────────────────────────────────────────────────────────────
3269// PORT STATUS
3270//   source:        src/lvm.c  (1899 lines, 32 functions)
3271//   target_crate:  lua-vm
3272//   confidence:    medium
3273//   todos:         6
3274//   port_notes:    4
3275//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
3276//   notes:         All opcode handlers and helpers translated; LuaState methods
3277//                  referenced (fast_get, precall, poscall, etc.) are stubs that
3278//                  Phase B will land.  The execute() goto flow is modelled with
3279//                  labelled Rust loops ('startfunc/'returning/'dispatch).
3280//                  str_to_number is a stub pending luaO_str2num port (TODO #1).
3281//                  strcoll replaced with byte-lexicographic order (TODO #2).
3282//                  order_imm_op uses LuaValue as a stand-in for GcRef<LuaClosure>
3283//                  (TODO #3).  ClosureRef type alias not yet defined (TODO #4-6).
3284// ──────────────────────────────────────────────────────────────────────────