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/// Increments the float loop index and returns `true` if the loop continues.
797fn float_for_loop(state: &mut LuaState, ra: StackIdx) -> bool {
798    //    idx  = fltvalue(s2v(ra));
799    let step = match state.get_at(ra + 2) {
800        LuaValue::Float(f) => f,
801        _ => return false,
802    };
803    let limit = match state.get_at(ra + 1) {
804        LuaValue::Float(f) => f,
805        _ => return false,
806    };
807    let idx = match state.get_at(ra) {
808        LuaValue::Float(f) => f,
809        _ => return false,
810    };
811    let idx = idx + step;
812    if if step > 0.0 { idx <= limit } else { limit <= idx } {
813        state.set_at(ra,     LuaValue::Float(idx));
814        state.set_at(ra + 3, LuaValue::Float(idx));
815        true
816    } else {
817        false
818    }
819}
820
821// ─── Table get/set with metamethod chains ────────────────────────────────────
822
823/// StkId val, const TValue *slot)`
824/// Finish a table-get with metamethod lookup.  `slot_was_none = true` means
825/// `t` is not a table and we should look for `__index` on `t` itself.
826pub(crate) fn finish_get(
827    state: &mut LuaState,
828    t_val: LuaValue,
829    key: LuaValue,
830    result_idx: StackIdx,
831    slot_empty: bool,
832    t_idx: Option<StackIdx>,
833) -> Result<(), LuaError> {
834    let mut t = t_val;
835    let mut t_idx = t_idx;
836    for _loop in 0..MAX_TAG_LOOP {
837        let tm: LuaValue;
838        if slot_empty && !matches!(t, LuaValue::Table(_)) {
839            tm = state.get_tm_by_obj(&t, TagMethod::Index);
840            if matches!(tm, LuaValue::Nil) {
841                return Err(match t_idx {
842                    Some(idx) => crate::debug::type_error(state, &t, idx, b"index"),
843                    None => LuaError::type_error(&t, "index"),
844                });
845            }
846        } else {
847            let mt = state.table_metatable(&t);
848            tm = state.fast_tm_table(mt.as_ref(), TagMethod::Index);
849            if matches!(tm, LuaValue::Nil) {
850                state.set_at(result_idx, LuaValue::Nil);
851                return Ok(());
852            }
853        }
854        if matches!(tm, LuaValue::Function(_)) {
855            state.call_tm_res(tm, &t, &key, result_idx)?;
856            return Ok(());
857        }
858        t = tm.clone();
859        t_idx = None;
860        if let Some(v) = state.fast_get(&t, &key)? {
861            state.set_at(result_idx, v);
862            return Ok(());
863        }
864        // else: loop — tail-call luaV_finishget
865    }
866    Err(LuaError::runtime(format_args!("'__index' chain too long; possible loop")))
867}
868
869/// TValue *val, const TValue *slot)`
870/// Finish a table-set with `__newindex` metamethod lookup.
871///
872/// `var_hint` carries a `(kind, name)` pair (e.g. `(b"upvalue", b"a")`) used
873/// only when `t_idx` is None and the target is non-indexable — typically
874/// when the LHS is an upvalue (OP_SETTABUP). Pointer-identifying var_info
875/// won't recover the upvalue's name in that case, so the caller passes it
876/// in directly.
877pub(crate) fn finish_set(
878    state: &mut LuaState,
879    t_val: LuaValue,
880    key: LuaValue,
881    val: LuaValue,
882    _slot_present: bool,
883    t_idx: Option<StackIdx>,
884    var_hint: Option<(&[u8], &[u8])>,
885) -> Result<(), LuaError> {
886    let mut t = t_val;
887    let mut t_idx = t_idx;
888    for _loop in 0..MAX_TAG_LOOP {
889        let tm: LuaValue;
890        if matches!(t, LuaValue::Table(_)) {
891            let mt = state.table_metatable(&t);
892            tm = state.fast_tm_table(mt.as_ref(), TagMethod::NewIndex);
893            if matches!(tm, LuaValue::Nil) {
894                state.table_raw_set(&t, key, val.clone())?;
895                state.gc_barrier_back(&t, &val);
896                return Ok(());
897            }
898        } else {
899            tm = state.get_tm_by_obj(&t, TagMethod::NewIndex);
900            if matches!(tm, LuaValue::Nil) {
901                return Err(match (t_idx, var_hint) {
902                    (Some(idx), _) => crate::debug::type_error(state, &t, idx, b"index"),
903                    (None, Some((kind, name))) => {
904                        crate::debug::type_error_with_hint(state, &t, b"index", kind, name)
905                    }
906                    (None, None) => LuaError::type_error(&t, "index"),
907                });
908            }
909        }
910        if matches!(tm, LuaValue::Function(_)) {
911            state.call_tm(tm, &t, &key, &val)?;
912            return Ok(());
913        }
914        t = tm.clone();
915        t_idx = None;
916        if state.fast_get(&t, &key)?.is_some() {
917            state.table_raw_set(&t, key.clone(), val.clone())?;
918            state.gc_barrier_back(&t, &val);
919            return Ok(());
920        }
921    }
922    Err(LuaError::runtime(format_args!("'__newindex' chain too long; possible loop")))
923}
924
925// ─── String comparison ───────────────────────────────────────────────────────
926
927/// Lexicographic string comparison that handles embedded NULs by segmenting.
928/// Returns negative / zero / positive like `strcmp`.
929///
930/// PORT NOTE: C uses `strcoll` for locale-aware comparison within each NUL-free
931/// segment.  Rust's standard library has no locale support, so we use
932/// `slice::cmp` (byte-by-byte lexicographic order, equivalent to `memcmp`).
933/// This means locale-specific ordering (e.g. accented characters) differs from
934/// the C reference.  Mark as TODO for a later `libc::strcoll` bridge if needed.
935fn str_cmp(s1: &[u8], s2: &[u8]) -> std::cmp::Ordering {
936    // TODO(port): C uses strcoll per-segment; here we use byte-lexicographic
937    // order.  This affects locale-sensitive string comparisons.
938    let mut s1 = s1;
939    let mut s2 = s2;
940    loop {
941        // Find the first NUL in each slice to delimit a segment.
942        let z1 = s1.iter().position(|&b| b == 0).unwrap_or(s1.len());
943        let z2 = s2.iter().position(|&b| b == 0).unwrap_or(s2.len());
944        // Compare segment up to first NUL using byte order (not strcoll).
945        let seg_cmp = s1[..z1].cmp(&s2[..z2]);
946        if seg_cmp != std::cmp::Ordering::Equal {
947            return seg_cmp;
948        }
949        // Both segments compare equal up to the NUL position.
950        if z2 == s2.len() {
951            // s2 is finished
952            if z1 == s1.len() {
953                return std::cmp::Ordering::Equal;
954            }
955            return std::cmp::Ordering::Greater; // s1 has more
956        }
957        if z1 == s1.len() {
958            return std::cmp::Ordering::Less; // s1 finished, s2 has more
959        }
960        // Both have NULs; advance past them.
961        s1 = &s1[z1 + 1..];
962        s2 = &s2[z2 + 1..];
963    }
964}
965
966// ─── Comparison helpers (int vs float mixed comparisons) ────────────────────
967
968#[inline]
969fn lt_int_float(i: i64, f: f64) -> bool {
970    if int_fits_float(i) {
971        (i as f64) < f
972    } else {
973        match flt_to_integer(f, F2Imod::Ceil) {
974            Some(fi) => i < fi,
975            None => f > 0.0, // f is out of integer range; positive means i < f
976        }
977    }
978}
979
980#[inline]
981fn le_int_float(i: i64, f: f64) -> bool {
982    if int_fits_float(i) {
983        (i as f64) <= f
984    } else {
985        match flt_to_integer(f, F2Imod::Floor) {
986            Some(fi) => i <= fi,
987            None => f > 0.0,
988        }
989    }
990}
991
992#[inline]
993fn lt_float_int(f: f64, i: i64) -> bool {
994    if int_fits_float(i) {
995        f < (i as f64)
996    } else {
997        match flt_to_integer(f, F2Imod::Floor) {
998            Some(fi) => fi < i,
999            None => f < 0.0,
1000        }
1001    }
1002}
1003
1004#[inline]
1005fn le_float_int(f: f64, i: i64) -> bool {
1006    if int_fits_float(i) {
1007        f <= (i as f64)
1008    } else {
1009        match flt_to_integer(f, F2Imod::Ceil) {
1010            Some(fi) => fi <= i,
1011            None => f < 0.0,
1012        }
1013    }
1014}
1015
1016#[inline]
1017fn lt_num(l: &LuaValue, r: &LuaValue) -> bool {
1018    debug_assert!(matches!(l, LuaValue::Int(_) | LuaValue::Float(_)));
1019    debug_assert!(matches!(r, LuaValue::Int(_) | LuaValue::Float(_)));
1020    match (l, r) {
1021        (LuaValue::Int(li), LuaValue::Int(ri))     => li < ri,
1022        (LuaValue::Int(li), LuaValue::Float(rf))   => lt_int_float(*li, *rf),
1023        (LuaValue::Float(lf), LuaValue::Float(rf)) => lf < rf,
1024        (LuaValue::Float(lf), LuaValue::Int(ri))   => lt_float_int(*lf, *ri),
1025        _ => false,
1026    }
1027}
1028
1029#[inline]
1030fn le_num(l: &LuaValue, r: &LuaValue) -> bool {
1031    debug_assert!(matches!(l, LuaValue::Int(_) | LuaValue::Float(_)));
1032    debug_assert!(matches!(r, LuaValue::Int(_) | LuaValue::Float(_)));
1033    match (l, r) {
1034        (LuaValue::Int(li), LuaValue::Int(ri))     => li <= ri,
1035        (LuaValue::Int(li), LuaValue::Float(rf))   => le_int_float(*li, *rf),
1036        (LuaValue::Float(lf), LuaValue::Float(rf)) => lf <= rf,
1037        (LuaValue::Float(lf), LuaValue::Int(ri))   => le_float_int(*lf, *ri),
1038        _ => false,
1039    }
1040}
1041
1042/// `l < r` for non-numbers (strings or metamethod).
1043fn less_than_others(state: &mut LuaState, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
1044    debug_assert!(!(matches!(l, LuaValue::Int(_) | LuaValue::Float(_))
1045                  && matches!(r, LuaValue::Int(_) | LuaValue::Float(_))));
1046    match (l, r) {
1047        (LuaValue::Str(ts1), LuaValue::Str(ts2)) => {
1048            Ok(str_cmp(ts1.as_bytes(), ts2.as_bytes()) == std::cmp::Ordering::Less)
1049        }
1050        _ => state.call_order_tm(l, r, TagMethod::Lt),
1051    }
1052}
1053
1054pub(crate) fn less_than(state: &mut LuaState, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
1055    if matches!(l, LuaValue::Int(_) | LuaValue::Float(_))
1056        && matches!(r, LuaValue::Int(_) | LuaValue::Float(_))
1057    {
1058        Ok(lt_num(l, r))
1059    } else {
1060        less_than_others(state, l, r)
1061    }
1062}
1063
1064fn less_equal_others(state: &mut LuaState, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
1065    match (l, r) {
1066        (LuaValue::Str(ts1), LuaValue::Str(ts2)) => {
1067            Ok(str_cmp(ts1.as_bytes(), ts2.as_bytes()) != std::cmp::Ordering::Greater)
1068        }
1069        _ => state.call_order_tm(l, r, TagMethod::Le),
1070    }
1071}
1072
1073pub(crate) fn less_equal(state: &mut LuaState, l: &LuaValue, r: &LuaValue) -> Result<bool, LuaError> {
1074    if matches!(l, LuaValue::Int(_) | LuaValue::Float(_))
1075        && matches!(r, LuaValue::Int(_) | LuaValue::Float(_))
1076    {
1077        Ok(le_num(l, r))
1078    } else {
1079        less_equal_others(state, l, r)
1080    }
1081}
1082
1083// ─── Equality ────────────────────────────────────────────────────────────────
1084
1085/// Main equality test.  `raw = true` means no metamethods (L == NULL in C).
1086pub(crate) fn equal_obj(
1087    state: Option<&mut LuaState>,
1088    t1: &LuaValue,
1089    t2: &LuaValue,
1090) -> Result<bool, LuaError> {
1091    // In Rust, same variant = same tag.  If variant differs, check the number
1092    // special case (Int and Float can be equal).
1093    let same_variant = std::mem::discriminant(t1) == std::mem::discriminant(t2);
1094    if !same_variant {
1095        let t1_is_num = matches!(t1, LuaValue::Int(_) | LuaValue::Float(_));
1096        let t2_is_num = matches!(t2, LuaValue::Int(_) | LuaValue::Float(_));
1097        if !(t1_is_num && t2_is_num) {
1098            return Ok(false);
1099        }
1100        // luaV_tointegerns(t1, &i1, F2Ieq) && luaV_tointegerns(t2, &i2, F2Ieq) && i1==i2
1101        let i1 = to_integer_ns(t1, F2Imod::Eq);
1102        let i2 = to_integer_ns(t2, F2Imod::Eq);
1103        return Ok(i1.is_some() && i2.is_some() && i1 == i2);
1104    }
1105
1106    match (t1, t2) {
1107        (LuaValue::Nil,  LuaValue::Nil)  => Ok(true),
1108        (LuaValue::Bool(b1), LuaValue::Bool(b2)) => Ok(b1 == b2),
1109        (LuaValue::Int(i1), LuaValue::Int(i2)) => Ok(i1 == i2),
1110        (LuaValue::Float(f1), LuaValue::Float(f2)) => Ok(f1 == f2),
1111        (LuaValue::LightUserData(p1), LuaValue::LightUserData(p2)) => Ok(p1 == p2),
1112        (LuaValue::Function(f1), LuaValue::Function(f2)) => {
1113            use lua_types::closure::LuaClosure;
1114            let same = match (f1, f2) {
1115                (LuaClosure::Lua(a), LuaClosure::Lua(b)) => GcRef::ptr_eq(a, b),
1116                (LuaClosure::C(a), LuaClosure::C(b)) => GcRef::ptr_eq(a, b),
1117                (LuaClosure::LightC(a), LuaClosure::LightC(b)) => a == b,
1118                _ => false,
1119            };
1120            Ok(same)
1121        }
1122        (LuaValue::Str(s1), LuaValue::Str(s2)) => {
1123            //    luaS_eqlngstr for long strings (content eq).
1124            // In Rust, LuaString PartialEq handles both.
1125            Ok(s1 == s2)
1126        }
1127        (LuaValue::UserData(u1), LuaValue::UserData(u2)) => {
1128            //    else if (L == NULL) return 0;
1129            //    tm = fasttm(L, uvalue(t1)->metatable, TM_EQ);
1130            if std::ptr::eq(u1.as_ptr(), u2.as_ptr()) {
1131                return Ok(true);
1132            }
1133            let Some(state) = state else { return Ok(false); };
1134            let tm1 = state.fast_tm_ud(u1, TagMethod::Eq);
1135            let tm = if matches!(tm1, LuaValue::Nil) {
1136                state.fast_tm_ud(u2, TagMethod::Eq)
1137            } else {
1138                tm1
1139            };
1140            if matches!(tm, LuaValue::Nil) {
1141                return Ok(false);
1142            }
1143            let result = state.call_tm_res_bool(tm, t1, t2)?;
1144            Ok(result)
1145        }
1146        (LuaValue::Table(h1), LuaValue::Table(h2)) => {
1147            if std::ptr::eq(h1.as_ptr(), h2.as_ptr()) {
1148                return Ok(true);
1149            }
1150            let Some(state) = state else { return Ok(false); };
1151            //    if (tm == NULL) tm = fasttm(L, hvalue(t2)->metatable, TM_EQ);
1152            let mt1 = h1.metatable();
1153            let mt2 = h2.metatable();
1154            let tm1 = state.fast_tm_table(mt1.as_ref(), TagMethod::Eq);
1155            let tm = if matches!(tm1, LuaValue::Nil) {
1156                state.fast_tm_table(mt2.as_ref(), TagMethod::Eq)
1157            } else {
1158                tm1
1159            };
1160            if matches!(tm, LuaValue::Nil) {
1161                return Ok(false);
1162            }
1163            let result = state.call_tm_res_bool(tm, t1, t2)?;
1164            Ok(result)
1165        }
1166        (LuaValue::Thread(a), LuaValue::Thread(b)) => Ok(GcRef::ptr_eq(a, b)),
1167        _ => Ok(std::ptr::eq(t1 as *const _, t2 as *const _)),
1168    }
1169}
1170
1171// ─── Concatenation ───────────────────────────────────────────────────────────
1172
1173/// Copy `n` strings from `top-n .. top-1` into `buff`.
1174fn copy_to_buf(state: &LuaState, top: StackIdx, n: u32, buf: &mut Vec<u8>) {
1175    buf.clear();
1176    let mut remaining = n;
1177    loop {
1178        let idx = top - remaining as i32;
1179        let v = state.get_at(idx);
1180        if let LuaValue::Str(ts) = v {
1181            buf.extend_from_slice(ts.as_bytes());
1182        }
1183        if remaining <= 1 {
1184            break;
1185        }
1186        remaining -= 1;
1187    }
1188}
1189
1190/// Concatenate `total` values on the top of the stack, leaving one result.
1191pub(crate) fn concat(state: &mut LuaState, total: i32) -> Result<(), LuaError> {
1192    if total == 1 {
1193        return Ok(());
1194    }
1195    let mut total = total;
1196    loop {
1197        let top = state.top_idx();
1198        let v_tm1 = state.get_at(top - 1); // top-1
1199        let v_tm2 = state.get_at(top - 2); // top-2
1200
1201        //    luaT_tryconcatTM(L);
1202        let top2_coercible = matches!(v_tm2, LuaValue::Str(_))
1203            || matches!(v_tm2, LuaValue::Int(_) | LuaValue::Float(_));
1204        // tostring converts numbers to strings; we check top-1 too
1205        let top1_stringlike = matches!(v_tm1, LuaValue::Str(_))
1206            || matches!(v_tm1, LuaValue::Int(_) | LuaValue::Float(_));
1207        if !top2_coercible || !top1_stringlike {
1208            state.try_concat_tm(&v_tm1, &v_tm2)?;
1209            // at the bottom of the do-while runs for this branch too.
1210            // The metamethod writes its single result to top-2, leaving
1211            // top-1 stale; popping that stale slot is what makes the next
1212            // iteration see the just-computed result at the new top-1.
1213            total -= 1;
1214            let top = state.top_idx();
1215            state.set_top(top - 1);
1216            if total <= 1 {
1217                break;
1218            }
1219            continue;
1220        }
1221
1222        let is_empty = |v: &LuaValue| -> bool {
1223            matches!(v, LuaValue::Str(s) if s.as_bytes().is_empty())
1224        };
1225
1226        let n: u32;
1227        if is_empty(&v_tm1) {
1228            state.coerce_to_string(top - 2)?;
1229            n = 2;
1230        } else if is_empty(&v_tm2) {
1231            // so top-1 is guaranteed to be a string here. We replicate that
1232            // conversion before the copy so numbers don't leak through.
1233            state.coerce_to_string(top - 1)?;
1234            let v = state.get_at(top - 1);
1235            state.set_at(top - 2, v);
1236            n = 2;
1237        } else {
1238            // Ensure top-1 is a string (coerce if number)
1239            state.coerce_to_string(top - 1)?;
1240            let s1 = match state.get_at(top - 1) {
1241                LuaValue::Str(ts) => ts.as_bytes().len(),
1242                _ => 0,
1243            };
1244            let mut total_len = s1;
1245            let mut count: u32 = 1;
1246            let top = state.top_idx();
1247            loop {
1248                if count as i32 >= total {
1249                    break;
1250                }
1251                let idx = top - (count as i32 + 1);
1252                let v = state.get_at(idx);
1253                if !matches!(v, LuaValue::Str(_) | LuaValue::Int(_) | LuaValue::Float(_)) {
1254                    break;
1255                }
1256                state.coerce_to_string(idx)?;
1257                let l = match state.get_at(idx) {
1258                    LuaValue::Str(ts) => ts.as_bytes().len(),
1259                    _ => 0,
1260                };
1261                if l >= usize::MAX - total_len {
1262                    // pop strings to avoid wasting stack
1263                    state.set_top(top - total as i32);
1264                    return Err(LuaError::runtime(format_args!("string length overflow")));
1265                }
1266                total_len += l;
1267                count += 1;
1268            }
1269            n = count;
1270
1271            // Build concatenated result
1272            let mut buf: Vec<u8> = Vec::with_capacity(total_len);
1273            let top = state.top_idx();
1274            copy_to_buf(state, top, n, &mut buf);
1275            let ts = state.intern_or_create_str(&buf)?;
1276            state.set_at(top - n as i32, LuaValue::Str(ts));
1277        }
1278        total -= n as i32 - 1;
1279        let top = state.top_idx();
1280        state.set_top(top - ((n - 1) as i32));
1281
1282        if total <= 1 {
1283            break;
1284        }
1285    }
1286    Ok(())
1287}
1288
1289// ─── Object length ───────────────────────────────────────────────────────────
1290
1291/// Main implementation of the `#` operator.
1292pub(crate) fn obj_len(state: &mut LuaState, ra: StackIdx, rb: LuaValue, rb_idx: StackIdx) -> Result<(), LuaError> {
1293    match &rb {
1294        LuaValue::Table(_) => {
1295            //    if (tm) break; else setivalue(s2v(ra), luaH_getn(h));
1296            // Lua 5.1 `#t` never consults a table `__len` metamethod (only
1297            // userdata can intercept `#` there); `__len` on tables was added in
1298            // 5.2. Under V51 we therefore always take the primitive length.
1299            let consult_len_tm = !matches!(
1300                state.global().lua_version,
1301                lua_types::LuaVersion::V51
1302            );
1303            let tm = if consult_len_tm {
1304                let mt = state.table_metatable(&rb);
1305                state.fast_tm_table(mt.as_ref(), TagMethod::Len)
1306            } else {
1307                LuaValue::Nil
1308            };
1309            if matches!(tm, LuaValue::Nil) {
1310                let n = state.table_length(&rb)?;
1311                state.set_at(ra, LuaValue::Int(n as i64));
1312                return Ok(());
1313            }
1314            // Fall through to call metamethod
1315            state.call_tm_res(tm, &rb, &rb, ra)?;
1316        }
1317        LuaValue::Str(ts) => {
1318            //    case LUA_VLNGSTR: setivalue(s2v(ra), tsvalue(rb)->u.lnglen);
1319            // Unified in Rust — just get length
1320            let n = ts.len();
1321            state.set_at(ra, LuaValue::Int(n as i64));
1322        }
1323        other => {
1324            //    if (notm(tm)) luaG_typeerror(L, rb, "get length of");
1325            let tm = state.get_tm_by_obj(other, TagMethod::Len);
1326            if matches!(tm, LuaValue::Nil) {
1327                return Err(crate::debug::type_error(state, other, rb_idx, b"get length of"));
1328            }
1329            state.call_tm_res(tm, &rb, &rb, ra)?;
1330        }
1331    }
1332    Ok(())
1333}
1334
1335// ─── Integer arithmetic ──────────────────────────────────────────────────────
1336
1337/// Integer floor-division.
1338pub(crate) fn idiv(m: i64, n: i64) -> Result<i64, LuaError> {
1339    if (n as u64).wrapping_add(1) <= 1 {
1340        if n == 0 {
1341            return Err(LuaError::runtime(format_args!("attempt to divide by zero")));
1342        }
1343        return Ok(intop_sub(0, m));
1344    }
1345    let q = m / n;
1346    // Correct toward floor (C division truncates toward zero)
1347    if (m ^ n) < 0 && m % n != 0 {
1348        Ok(q - 1)
1349    } else {
1350        Ok(q)
1351    }
1352}
1353
1354/// Integer modulus (Lua semantics: same sign as divisor).
1355pub(crate) fn imod(m: i64, n: i64) -> Result<i64, LuaError> {
1356    if (n as u64).wrapping_add(1) <= 1 {
1357        if n == 0 {
1358            return Err(LuaError::runtime(format_args!("attempt to perform 'n%0'")));
1359        }
1360        return Ok(0);
1361    }
1362    let r = m % n;
1363    if r != 0 && (r ^ n) < 0 {
1364        Ok(r + n)
1365    } else {
1366        Ok(r)
1367    }
1368}
1369
1370/// Float modulus (Lua semantics).
1371pub(crate) fn fmodf(m: f64, n: f64) -> f64 {
1372    let r = m % n;
1373    let opposite_signs = if r > 0.0 { n < 0.0 } else { r < 0.0 && n > 0.0 };
1374    if opposite_signs {
1375        r + n
1376    } else {
1377        r
1378    }
1379}
1380
1381/// Phase-B helper: map a u8 raw value to a `TagMethod`. Mirrors C's
1382/// `cast(TMS, x)` direct cast; out-of-range returns `TagMethod::Index`.
1383pub(crate) fn tagmethod_from_index(i: usize) -> TagMethod {
1384    use TagMethod::*;
1385    match i {
1386        0 => Index, 1 => NewIndex, 2 => Gc, 3 => Mode, 4 => Len, 5 => Eq,
1387        6 => Add, 7 => Sub, 8 => Mul, 9 => Mod, 10 => Pow, 11 => Div,
1388        12 => Idiv, 13 => Band, 14 => Bor, 15 => Bxor, 16 => Shl, 17 => Shr,
1389        18 => Unm, 19 => Bnot, 20 => Lt, 21 => Le, 22 => Concat, 23 => Call,
1390        24 => Close,
1391        _ => Index,
1392    }
1393}
1394
1395/// Integer floor-mod: Lua's `%` operator on integers. Result has the same sign
1396/// as the divisor. Raises on `n == 0`.
1397pub(crate) fn int_floor_mod(_state: &mut LuaState, a: i64, b: i64) -> Result<i64, LuaError> {
1398    imod(a, b)
1399}
1400
1401/// Integer floor-div: Lua's `//` operator on integers. Truncates toward
1402/// negative infinity. Raises on `n == 0`.
1403pub(crate) fn int_floor_div(_state: &mut LuaState, a: i64, b: i64) -> Result<i64, LuaError> {
1404    idiv(a, b)
1405}
1406
1407/// Float floor-mod: Lua's `%` operator on floats. Result has the same sign as
1408/// the divisor.  NaN / division-by-zero behavior mirrors C `fmod`.
1409pub(crate) fn float_floor_mod(_state: &mut LuaState, a: f64, b: f64) -> Result<f64, LuaError> {
1410    Ok(fmodf(a, b))
1411}
1412
1413/// Left shift; right shift is shift-left by negated count.
1414pub(crate) fn shiftl(x: i64, y: i64) -> i64 {
1415    if y < 0 {
1416        if y <= -(NBITS as i64) {
1417            0
1418        } else {
1419            intop_shr(x, (-y) as u32)
1420        }
1421    } else {
1422        if y >= NBITS as i64 {
1423            0
1424        } else {
1425            intop_shl(x, y as u32)
1426        }
1427    }
1428}
1429
1430// ─── Closure creation ────────────────────────────────────────────────────────
1431
1432/// StkId base, StkId ra)`
1433/// Create a new Lua closure from prototype `p`, initialise its upvalues,
1434/// and push it onto the stack at `ra`.
1435fn push_closure(
1436    state: &mut LuaState,
1437    proto_idx: usize,   // index into current closure's proto.p[]
1438    ci: CallInfoIdx,
1439    base: StackIdx,
1440    ra: StackIdx,
1441) -> Result<(), LuaError> {
1442    // TODO(port): pushclosure needs access to the enclosing closure's upvals and
1443    // the child proto from the current frame.  This stub forwards to a LuaState
1444    // method that has the required context.
1445    state.push_closure(proto_idx, ci, base, ra)
1446}
1447
1448// ─── Yield recovery ──────────────────────────────────────────────────────────
1449
1450/// Resume the opcode that was interrupted by a yield.
1451/// Called when a coroutine is resumed after yielding mid-instruction.
1452pub(crate) fn finish_op(state: &mut LuaState) -> Result<(), LuaError> {
1453    //    StkId base = ci->func.p + 1;
1454    //    Instruction inst = *(ci->u.l.savedpc - 1);
1455    //    OpCode op = GET_OPCODE(inst);
1456    let ci = state.current_ci_idx();
1457    let base = state.ci_base(ci);
1458    let inst = state.ci_prev_instruction(ci);
1459    let op = inst.opcode();
1460
1461    match op {
1462        //    setobjs2s(L, base + GETARG_A(*(ci->u.l.savedpc - 2)), --L->top.p);
1463        OpCode::MmBin | OpCode::MmBinI | OpCode::MmBinK => {
1464            let prev_inst = state.ci_prev2_instruction(ci);
1465            let a = prev_inst.arg_a();
1466            state.dec_top();
1467            let top = state.top_idx();
1468            let v = state.get_at(top);
1469            state.set_at(base + a, v);
1470        }
1471        //    setobjs2s(L, base + GETARG_A(inst), --L->top.p);
1472        OpCode::Unm | OpCode::BNot | OpCode::Len
1473        | OpCode::GetTabUp | OpCode::GetTable | OpCode::GetI
1474        | OpCode::GetField | OpCode::Self_ => {
1475            let a = inst.arg_a();
1476            state.dec_top();
1477            let top = state.top_idx();
1478            let v = state.get_at(top);
1479            state.set_at(base + a, v);
1480        }
1481        //    case OP_GTI: case OP_GEI: case OP_EQ:
1482        //    int res = !l_isfalse(s2v(L->top.p - 1)); L->top.p--;
1483        //    if (res != GETARG_k(inst)) ci->u.l.savedpc++;
1484        OpCode::Lt | OpCode::Le | OpCode::LtI | OpCode::LeI
1485        | OpCode::GtI | OpCode::GeI | OpCode::Eq => {
1486            let top_minus1 = state.top_idx() - 1;
1487            let v = state.get_at(top_minus1);
1488            let mut res = !matches!(v, LuaValue::Nil | LuaValue::Bool(false));
1489            state.dec_top();
1490            // LUA_COMPAT_LT_LE: if this `__le` was derived from a `__lt` that
1491            // yielded (5.1–5.4), the result `b < a` must be negated back to
1492            // `a <= b`. The mark was set in `tagmethods::call_order_tm`.
1493            // C (lvm.c luaV_finishOp): `if (callstatus & CIST_LEQ) { ^= ; res = !res; }`
1494            if (state.get_ci(ci).callstatus & crate::state::CIST_LEQ) != 0 {
1495                state.get_ci_mut(ci).callstatus &= !crate::state::CIST_LEQ;
1496                res = !res;
1497            }
1498            if (res as i32) != inst.arg_k() {
1499                state.ci_skip_next_instruction(ci);
1500            }
1501        }
1502        //    StkId top = L->top.p - 1;
1503        //    int a = GETARG_A(inst);
1504        //    int total = cast_int(top - 1 - (base + a));
1505        //    setobjs2s(L, top - 2, top);  L->top.p = top - 1;
1506        //    luaV_concat(L, total);
1507        OpCode::Concat => {
1508            let top = state.top_idx() - 1; // top when luaT_tryconcatTM was called
1509            let a = inst.arg_a();
1510            let total_concat = (top - 1 - (base + a)) as i32;
1511            let v = state.get_at(top);
1512            state.set_at(top - 2, v);
1513            state.set_top(top - 1);
1514            concat(state, total_concat)?;
1515        }
1516        OpCode::Close => {
1517            state.ci_step_pc_back(ci);
1518        }
1519        //    StkId ra = base + GETARG_A(inst);
1520        //    L->top.p = ra + ci->u2.nres;
1521        //    ci->u.l.savedpc--;
1522        OpCode::Return => {
1523            let a = inst.arg_a();
1524            let ra = base + a;
1525            let nres = state.ci_nres(ci);
1526            state.set_top(ra + nres);
1527            state.ci_step_pc_back(ci);
1528        }
1529        other => {
1530            debug_assert!(
1531                matches!(
1532                    other,
1533                    OpCode::TForCall | OpCode::Call | OpCode::TailCall
1534                    | OpCode::SetTabUp | OpCode::SetTable | OpCode::SetI | OpCode::SetField
1535                ),
1536                "unexpected opcode in finish_op: {:?}",
1537                other
1538            );
1539        }
1540    }
1541    Ok(())
1542}
1543
1544// ─── Main interpreter loop ───────────────────────────────────────────────────
1545
1546/// Main Lua bytecode interpreter loop.
1547///
1548/// # Control flow modelling
1549/// The C function uses goto labels: `startfunc`, `returning`, `ret`,
1550/// `l_tforcall`, `l_tforloop`.  These are modelled as follows:
1551/// - `'startfunc: loop { ... }` — outer loop; `continue 'startfunc` = goto startfunc
1552/// - `'returning: loop { ... }` — inner loop; `continue 'returning` = goto returning
1553/// - `break 'dispatch` from the inner dispatch loop → runs `ret:` logic
1554/// - `l_tforcall` / `l_tforloop` — inlined at TFORPREP / TFORCALL handlers
1555pub(crate) fn execute(state: &mut LuaState, mut ci: CallInfoIdx) -> Result<(), LuaError> {
1556    let mut trap: bool;
1557
1558    // PORT NOTE: `startfunc:` is the entry point that (re)sets `trap`.
1559    'startfunc: loop {
1560        trap = state.hook_mask() != 0;
1561
1562        // PORT NOTE: `returning:` is the re-entry after a Lua call returns.
1563        // Re-enters 'returning without resetting trap.
1564        'returning: loop {
1565            let cl = match state.ci_lua_closure(ci) {
1566                Some(c) => c,
1567                None => {
1568                    return Err(LuaError::runtime(format_args!(
1569                        "internal: execute called on non-Lua frame"
1570                    )));
1571                }
1572            };
1573            // pc is an index into proto.code (u32)
1574            let mut pc: u32 = state.ci_savedpc(ci);
1575
1576            if trap {
1577                trap = state.trace_call(ci)?;
1578            }
1579            let mut base: StackIdx = state.ci_base(ci);
1580
1581            // ── Main dispatch loop ──────────────────────────────────────────
1582            'dispatch: loop {
1583                if trap {
1584                    trap = state.trace_exec(ci, pc)?;
1585                    base = state.ci_base(ci); // updatebase
1586                }
1587                let i: Instruction = state.proto_code(&cl, pc);
1588                pc += 1;
1589                let op = i.opcode();
1590
1591                debug_assert!(base == state.ci_base(ci));
1592
1593                // In normal C-Lua builds, `lua_assert` compiles away; keep the
1594                // stack-top invalidation only for debug parity so release
1595                // dispatch avoids an opcode-mode lookup and a `top` write.
1596                #[cfg(debug_assertions)]
1597                {
1598                    let op_mode = op_mode_byte(op);
1599                    if (op_mode & (1 << 5)) == 0 || i.arg_b() != 0 {
1600                        state.set_top(base);
1601                    }
1602                }
1603
1604                match op {
1605                    // ── OP_MOVE ──────────────────────────────────────────────
1606                    OpCode::Move => {
1607                        let ra = base + i.arg_a();
1608                        let rb = base + i.arg_b();
1609                        let v = state.stack[rb.0 as usize].val;
1610                        state.stack[ra.0 as usize].val = v;
1611                    }
1612                    // ── OP_LOADI ─────────────────────────────────────────────
1613                    OpCode::LoadI => {
1614                        let ra = base + i.arg_a();
1615                        let b = i.arg_s_bx() as i64;
1616                        state.set_at(ra, LuaValue::Int(b));
1617                    }
1618                    // ── OP_LOADF ─────────────────────────────────────────────
1619                    OpCode::LoadF => {
1620                        let ra = base + i.arg_a();
1621                        let b = i.arg_s_bx() as f64;
1622                        state.set_at(ra, LuaValue::Float(b));
1623                    }
1624                    // ── OP_LOADK ─────────────────────────────────────────────
1625                    OpCode::LoadK => {
1626                        let ra = base + i.arg_a();
1627                        let k_idx = i.arg_bx() as usize;
1628                        let v = state.proto_const(&cl, k_idx).clone();
1629                        state.set_at(ra, v);
1630                    }
1631                    // ── OP_LOADKX ────────────────────────────────────────────
1632                    OpCode::LoadKX => {
1633                        let ra = base + i.arg_a();
1634                        let extra = state.proto_code(&cl, pc);
1635                        pc += 1;
1636                        let k_idx = extra.arg_ax() as usize;
1637                        let v = state.proto_const(&cl, k_idx).clone();
1638                        state.set_at(ra, v);
1639                    }
1640                    // ── OP_LOADFALSE ─────────────────────────────────────────
1641                    OpCode::LoadFalse => {
1642                        let ra = base + i.arg_a();
1643                        state.set_at(ra, LuaValue::Bool(false));
1644                    }
1645                    // ── OP_LFALSESKIP ────────────────────────────────────────
1646                    OpCode::LFalseSkip => {
1647                        let ra = base + i.arg_a();
1648                        state.set_at(ra, LuaValue::Bool(false));
1649                        pc += 1;
1650                    }
1651                    // ── OP_LOADTRUE ──────────────────────────────────────────
1652                    OpCode::LoadTrue => {
1653                        let ra = base + i.arg_a();
1654                        state.set_at(ra, LuaValue::Bool(true));
1655                    }
1656                    // ── OP_LOADNIL ───────────────────────────────────────────
1657                    OpCode::LoadNil => {
1658                        let ra = base + i.arg_a();
1659                        let b = i.arg_b();
1660                        for k in 0..=b {
1661                            state.set_at(ra + k, LuaValue::Nil);
1662                        }
1663                    }
1664                    // ── OP_GETUPVAL ──────────────────────────────────────────
1665                    OpCode::GetUpVal => {
1666                        let ra = base + i.arg_a();
1667                        let b = i.arg_b() as usize;
1668                        let v = state.upvalue_get(&cl, b);
1669                        state.set_at(ra, v);
1670                    }
1671                    // ── OP_SETUPVAL ──────────────────────────────────────────
1672                    //    setobj(L, uv->v.p, s2v(ra)); luaC_barrier(L, uv, s2v(ra));
1673                    OpCode::SetUpVal => {
1674                        let ra = base + i.arg_a();
1675                        let b = i.arg_b() as usize;
1676                        let v = state.stack[ra.0 as usize].val;
1677                        let uv = cl.upval(b);
1678                        match uv.try_open_payload() {
1679                            Some((thread_id, idx)) if thread_id as u64 == state.cached_thread_id => {
1680                                state.stack[idx.0 as usize].val = v;
1681                            }
1682                            _ => {
1683                                state.upvalue_set(&cl, b, v)?;
1684                            }
1685                        }
1686                    }
1687                    // ── OP_GETTABUP ──────────────────────────────────────────
1688                    //    if (luaV_fastget(..., luaH_getshortstr)) setobj2s(L, ra, slot)
1689                    //    else Protect(luaV_finishget(...))
1690                    OpCode::GetTabUp => {
1691                        let ra = base + i.arg_a();
1692                        let b = i.arg_b() as usize;
1693                        let k_idx = i.arg_c() as usize;
1694                        let upval = state.upvalue_get(&cl, b);
1695                        let key = state.proto_const(&cl, k_idx).clone();
1696                        match state.fast_get_short_str(&upval, &key)? {
1697                            Some(v) => state.set_at(ra, v),
1698                            None => {
1699                                state.set_ci_savedpc(ci, pc);
1700                                state.set_top(state.ci_top(ci));
1701                                finish_get(state, upval, key, ra, true, None)?;
1702                                trap = state.ci_trap(ci);
1703                            }
1704                        }
1705                    }
1706                    // ── OP_GETTABLE ──────────────────────────────────────────
1707                    //    if (integer key) fastgeti else fastget
1708                    OpCode::GetTable => {
1709                        let ra = base + i.arg_a();
1710                        let rb_idx = base + i.arg_b();
1711                        let rb_v = state.get_at(rb_idx);
1712                        let rc_v = state.get_at(base + i.arg_c());
1713                        let fast_result = if let LuaValue::Int(n) = &rc_v {
1714                            state.fast_get_int(&rb_v, *n)?
1715                        } else {
1716                            state.fast_get(&rb_v, &rc_v)?
1717                        };
1718                        match fast_result {
1719                            Some(v) => state.set_at(ra, v),
1720                            None => {
1721                                state.set_ci_savedpc(ci, pc);
1722                                state.set_top(state.ci_top(ci));
1723                                finish_get(state, rb_v, rc_v, ra, true, Some(rb_idx))?;
1724                                trap = state.ci_trap(ci);
1725                            }
1726                        }
1727                    }
1728                    // ── OP_GETI ──────────────────────────────────────────────
1729                    //    if (luaV_fastgeti(L, rb, c, slot)) setobj2s(L, ra, slot)
1730                    //    else { TValue key; setivalue(&key, c); Protect(finishget) }
1731                    OpCode::GetI => {
1732                        let ra = base + i.arg_a();
1733                        let rb_idx = base + i.arg_b();
1734                        let rb_v = state.get_at(rb_idx);
1735                        let c = i.arg_c() as i64;
1736                        match state.fast_get_int(&rb_v, c)? {
1737                            Some(v) => state.set_at(ra, v),
1738                            None => {
1739                                let key = LuaValue::Int(c);
1740                                state.set_ci_savedpc(ci, pc);
1741                                state.set_top(state.ci_top(ci));
1742                                finish_get(state, rb_v, key, ra, true, Some(rb_idx))?;
1743                                trap = state.ci_trap(ci);
1744                            }
1745                        }
1746                    }
1747                    // ── OP_GETFIELD ──────────────────────────────────────────
1748                    OpCode::GetField => {
1749                        let ra = base + i.arg_a();
1750                        let rb_idx = base + i.arg_b();
1751                        let rb_v = state.get_at(rb_idx);
1752                        let k_idx = i.arg_c() as usize;
1753                        let key = state.proto_const(&cl, k_idx).clone();
1754                        match state.fast_get_short_str(&rb_v, &key)? {
1755                            Some(v) => state.set_at(ra, v),
1756                            None => {
1757                                state.set_ci_savedpc(ci, pc);
1758                                state.set_top(state.ci_top(ci));
1759                                finish_get(state, rb_v, key, ra, true, Some(rb_idx))?;
1760                                trap = state.ci_trap(ci);
1761                            }
1762                        }
1763                    }
1764                    // ── OP_SETTABUP ──────────────────────────────────────────
1765                    OpCode::SetTabUp => {
1766                        let a = i.arg_a() as usize;
1767                        let b_idx = i.arg_b() as usize; // key is KB(i)
1768                        let rc_v = if i.test_k() {
1769                            state.proto_const(&cl, i.arg_c() as usize).clone()
1770                        } else {
1771                            state.get_at(base + i.arg_c())
1772                        };
1773                        let upval = state.upvalue_get(&cl, a);
1774                        let key = state.proto_const(&cl, b_idx).clone();
1775                        match state.fast_get_short_str(&upval, &key)? {
1776                            Some(_slot) => {
1777                                state.table_raw_set(&upval, key, rc_v.clone())?;
1778                                state.gc_barrier_back(&upval, &rc_v);
1779                            }
1780                            None => {
1781                                state.set_ci_savedpc(ci, pc);
1782                                state.set_top(state.ci_top(ci));
1783                                let upval_name: Vec<u8> = cl
1784                                    .proto
1785                                    .upvalues
1786                                    .get(a)
1787                                    .and_then(|uv| uv.name.as_ref())
1788                                    .map(|s| s.as_bytes().to_vec())
1789                                    .unwrap_or_else(|| b"?".to_vec());
1790                                let hint: Option<(&[u8], &[u8])> =
1791                                    Some((b"upvalue", &upval_name));
1792                                finish_set(state, upval, key, rc_v, false, None, hint)?;
1793                                trap = state.ci_trap(ci);
1794                            }
1795                        }
1796                    }
1797                    // ── OP_SETTABLE ───────────────────────────────────────────
1798                    OpCode::SetTable => {
1799                        let ra_idx = base + i.arg_a();
1800                        let ra_v = state.get_at(ra_idx);
1801                        let rb_v = state.get_at(base + i.arg_b());
1802                        let rc_v = if i.test_k() {
1803                            state.proto_const(&cl, i.arg_c() as usize).clone()
1804                        } else {
1805                            state.get_at(base + i.arg_c())
1806                        };
1807                        let fast = if let LuaValue::Int(n) = &rb_v {
1808                            state.fast_get_int(&ra_v, *n)?
1809                        } else {
1810                            state.fast_get(&ra_v, &rb_v)?
1811                        };
1812                        if fast.is_some() {
1813                            state.table_raw_set(&ra_v, rb_v, rc_v.clone())?;
1814                            state.gc_barrier_back(&ra_v, &rc_v);
1815                        } else {
1816                            state.set_ci_savedpc(ci, pc);
1817                            state.set_top(state.ci_top(ci));
1818                            finish_set(state, ra_v, rb_v, rc_v, false, Some(ra_idx), None)?;
1819                            trap = state.ci_trap(ci);
1820                        }
1821                    }
1822                    // ── OP_SETI ───────────────────────────────────────────────
1823                    OpCode::SetI => {
1824                        let ra_idx = base + i.arg_a();
1825                        let ra_v = state.get_at(ra_idx);
1826                        let c = i.arg_b() as i64;
1827                        let rc_v = if i.test_k() {
1828                            state.proto_const(&cl, i.arg_c() as usize).clone()
1829                        } else {
1830                            state.get_at(base + i.arg_c())
1831                        };
1832                        let fast = state.fast_get_int(&ra_v, c)?;
1833                        if fast.is_some() {
1834                            state.table_raw_set(&ra_v, LuaValue::Int(c), rc_v.clone())?;
1835                            state.gc_barrier_back(&ra_v, &rc_v);
1836                        } else {
1837                            state.set_ci_savedpc(ci, pc);
1838                            state.set_top(state.ci_top(ci));
1839                            finish_set(state, ra_v, LuaValue::Int(c), rc_v, false, Some(ra_idx), None)?;
1840                            trap = state.ci_trap(ci);
1841                        }
1842                    }
1843                    // ── OP_SETFIELD ───────────────────────────────────────────
1844                    OpCode::SetField => {
1845                        let ra_idx = base + i.arg_a();
1846                        let ra_v = state.get_at(ra_idx);
1847                        let b_idx = i.arg_b() as usize;
1848                        let key = state.proto_const(&cl, b_idx).clone();
1849                        let rc_v = if i.test_k() {
1850                            state.proto_const(&cl, i.arg_c() as usize).clone()
1851                        } else {
1852                            state.get_at(base + i.arg_c())
1853                        };
1854                        match state.fast_get_short_str(&ra_v, &key)? {
1855                            Some(_) => {
1856                                state.table_raw_set(&ra_v, key, rc_v.clone())?;
1857                                state.gc_barrier_back(&ra_v, &rc_v);
1858                            }
1859                            None => {
1860                                state.set_ci_savedpc(ci, pc);
1861                                state.set_top(state.ci_top(ci));
1862                                finish_set(state, ra_v, key, rc_v, false, Some(ra_idx), None)?;
1863                                trap = state.ci_trap(ci);
1864                            }
1865                        }
1866                    }
1867                    // ── OP_NEWTABLE ───────────────────────────────────────────
1868                    //    if (TESTARG_k(i)) c += GETARG_Ax(*pc) * (MAXARG_C + 1); pc++;
1869                    OpCode::NewTable => {
1870                        let ra = base + i.arg_a();
1871                        let mut b = i.arg_b();
1872                        let mut c = i.arg_c();
1873                        if b > 0 {
1874                            b = 1 << (b - 1);
1875                        }
1876                        if i.test_k() {
1877                            let extra = state.proto_code(&cl, pc);
1878                            pc += 1;
1879                            const MAXARG_C: i32 = (1 << 8) - 1;
1880                            c += extra.arg_ax() * (MAXARG_C + 1);
1881                        } else {
1882                            pc += 1; // skip extra argument even if zero
1883                        }
1884                        state.set_top(ra + 1);
1885                        let t = if b != 0 || c != 0 {
1886                            state.new_table_with_sizes(c as u32, b as u32)?
1887                        } else {
1888                            state.new_table()
1889                        };
1890                        state.set_at(ra, LuaValue::Table(t.clone()));
1891                        state.set_ci_savedpc(ci, pc);
1892                        state.set_top(ra + 1);
1893                        state.gc_cond_step();
1894                        if state.hookmask != 0 {
1895                            trap = state.ci_trap(ci);
1896                        }
1897                    }
1898                    // ── OP_SELF ───────────────────────────────────────────────
1899                    OpCode::Self_ => {
1900                        let ra = base + i.arg_a();
1901                        let rb_idx = base + i.arg_b();
1902                        let rb_v = state.get_at(rb_idx);
1903                        let k_idx = i.arg_c() as usize; // RKC key (always a string)
1904                        let key = if i.test_k() {
1905                            state.proto_const(&cl, k_idx).clone()
1906                        } else {
1907                            state.get_at(base + i.arg_c())
1908                        };
1909                        state.set_at(ra + 1, rb_v.clone());
1910                        match state.fast_get_short_str(&rb_v, &key)? {
1911                            Some(v) => state.set_at(ra, v),
1912                            None => {
1913                                state.set_ci_savedpc(ci, pc);
1914                                state.set_top(state.ci_top(ci));
1915                                finish_get(state, rb_v, key, ra, true, Some(rb_idx))?;
1916                                trap = state.ci_trap(ci);
1917                            }
1918                        }
1919                    }
1920                    // ── Arithmetic immediates ──────────────────────────────────
1921                    OpCode::AddI => {
1922                        let ra = base + i.arg_a();
1923                        let rb = base + i.arg_b();
1924                        let imm = i.arg_s_c() as i64;
1925                        let rb_v = state.stack[rb.0 as usize].val;
1926                        match rb_v {
1927                            LuaValue::Int(iv1) => {
1928                                pc += 1;
1929                                state.stack[ra.0 as usize].val = LuaValue::Int(intop_add(iv1, imm));
1930                            }
1931                            LuaValue::Float(nb) => {
1932                                pc += 1;
1933                                state.stack[ra.0 as usize].val = LuaValue::Float(nb + imm as f64);
1934                            }
1935                            _ => {}
1936                        }
1937                    }
1938                    // ── Arithmetic with K constant operand ─────────────────────
1939                    OpCode::AddK => {
1940                        let ra = base + i.arg_a();
1941                        let rb = base + i.arg_b();
1942                        let kidx = i.arg_c() as usize;
1943                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
1944                            pc += 1;
1945                            state.set_at(ra, LuaValue::Int(intop_add(i1, i2)));
1946                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1947                            pc += 1;
1948                            state.set_at(ra, LuaValue::Float(n1 + n2));
1949                        }
1950                    }
1951                    OpCode::SubK => {
1952                        let ra = base + i.arg_a();
1953                        let rb = base + i.arg_b();
1954                        let kidx = i.arg_c() as usize;
1955                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
1956                            pc += 1;
1957                            state.set_at(ra, LuaValue::Int(intop_sub(i1, i2)));
1958                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1959                            pc += 1;
1960                            state.set_at(ra, LuaValue::Float(n1 - n2));
1961                        }
1962                    }
1963                    OpCode::MulK => {
1964                        let ra = base + i.arg_a();
1965                        let rb = base + i.arg_b();
1966                        let kidx = i.arg_c() as usize;
1967                        if let (Some(i1), Some(i2)) = (state.get_int_at(rb), state.proto_const_int(&cl, kidx)) {
1968                            pc += 1;
1969                            state.set_at(ra, LuaValue::Int(intop_mul(i1, i2)));
1970                        } else if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1971                            pc += 1;
1972                            state.set_at(ra, LuaValue::Float(n1 * n2));
1973                        }
1974                    }
1975                    OpCode::ModK => {
1976                        let ra = base + i.arg_a();
1977                        let v1 = state.get_at(base + i.arg_b());
1978                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
1979                        state.set_ci_savedpc(ci, pc); // savestate for div-by-zero
1980                        state.set_top(state.ci_top(ci));
1981                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
1982                            |a, b| imod(a, b), fmodf)?;
1983                    }
1984                    OpCode::PowK => {
1985                        let ra = base + i.arg_a();
1986                        let rb = base + i.arg_b();
1987                        let kidx = i.arg_c() as usize;
1988                        if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1989                            pc += 1;
1990                            let r = if n2 == 2.0 { n1 * n1 } else { n1.powf(n2) };
1991                            state.set_at(ra, LuaValue::Float(r));
1992                        }
1993                    }
1994                    OpCode::DivK => {
1995                        let ra = base + i.arg_a();
1996                        let rb = base + i.arg_b();
1997                        let kidx = i.arg_c() as usize;
1998                        if let (Some(n1), Some(n2)) = (state.get_num_at(rb), state.proto_const_num(&cl, kidx)) {
1999                            pc += 1;
2000                            state.set_at(ra, LuaValue::Float(n1 / n2));
2001                        }
2002                    }
2003                    OpCode::IDivK => {
2004                        let ra = base + i.arg_a();
2005                        let v1 = state.get_at(base + i.arg_b());
2006                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
2007                        state.set_ci_savedpc(ci, pc);
2008                        state.set_top(state.ci_top(ci));
2009                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
2010                            |a, b| idiv(a, b), |a, b| (a / b).floor())?;
2011                    }
2012                    OpCode::BAndK => {
2013                        let ra = base + i.arg_a();
2014                        let v1 = state.get_at(base + i.arg_b());
2015                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
2016                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_band);
2017                    }
2018                    OpCode::BOrK => {
2019                        let ra = base + i.arg_a();
2020                        let v1 = state.get_at(base + i.arg_b());
2021                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
2022                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_bor);
2023                    }
2024                    OpCode::BXOrK => {
2025                        let ra = base + i.arg_a();
2026                        let v1 = state.get_at(base + i.arg_b());
2027                        let v2 = state.proto_const(&cl, i.arg_c() as usize).clone();
2028                        bitwise_op_k(state, ra, &v1, &v2, &mut pc, intop_bxor);
2029                    }
2030                    OpCode::ShrI => {
2031                        let ra = base + i.arg_a();
2032                        let v = state.get_at(base + i.arg_b());
2033                        let ic = i.arg_s_c() as i64;
2034                        if let Some(ib) = to_integer_ns(&v, F2Imod::Eq) {
2035                            pc += 1;
2036                            state.set_at(ra, LuaValue::Int(shiftl(ib, -ic)));
2037                        }
2038                    }
2039                    OpCode::ShlI => {
2040                        let ra = base + i.arg_a();
2041                        let v = state.get_at(base + i.arg_b());
2042                        let ic = i.arg_s_c() as i64;
2043                        if let Some(ib) = to_integer_ns(&v, F2Imod::Eq) {
2044                            pc += 1;
2045                            state.set_at(ra, LuaValue::Int(shiftl(ic, ib)));
2046                        }
2047                    }
2048                    // ── Arithmetic with register operands ──────────────────────
2049                    OpCode::Add => {
2050                        let ra = base + i.arg_a();
2051                        let rb = base + i.arg_b();
2052                        let rc = base + i.arg_c();
2053                        let ra_u = ra.0 as usize;
2054                        let rb_v = state.stack[rb.0 as usize].val;
2055                        let rc_v = state.stack[rc.0 as usize].val;
2056                        if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (rb_v, rc_v) {
2057                            pc += 1;
2058                            state.stack[ra_u].val = LuaValue::Int(intop_add(i1, i2));
2059                        } else if let (Some(n1), Some(n2)) = (number_value(rb_v), number_value(rc_v)) {
2060                            pc += 1;
2061                            state.stack[ra_u].val = LuaValue::Float(n1 + n2);
2062                        }
2063                    }
2064                    OpCode::Sub => {
2065                        let ra = base + i.arg_a();
2066                        let rb = base + i.arg_b();
2067                        let rc = base + i.arg_c();
2068                        let ra_u = ra.0 as usize;
2069                        let rb_v = state.stack[rb.0 as usize].val;
2070                        let rc_v = state.stack[rc.0 as usize].val;
2071                        if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (rb_v, rc_v) {
2072                            pc += 1;
2073                            state.stack[ra_u].val = LuaValue::Int(intop_sub(i1, i2));
2074                        } else if let (Some(n1), Some(n2)) = (number_value(rb_v), number_value(rc_v)) {
2075                            pc += 1;
2076                            state.stack[ra_u].val = LuaValue::Float(n1 - n2);
2077                        }
2078                    }
2079                    OpCode::Mul => {
2080                        let ra = base + i.arg_a();
2081                        let rb = base + i.arg_b();
2082                        let rc = base + i.arg_c();
2083                        if let Some((i1, i2)) = state.get_int_pair_at(rb, rc) {
2084                            pc += 1;
2085                            state.set_at(ra, LuaValue::Int(intop_mul(i1, i2)));
2086                        } else if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
2087                            pc += 1;
2088                            state.set_at(ra, LuaValue::Float(n1 * n2));
2089                        }
2090                    }
2091                    OpCode::Mod => {
2092                        let ra = base + i.arg_a();
2093                        let v1 = state.get_at(base + i.arg_b());
2094                        let v2 = state.get_at(base + i.arg_c());
2095                        state.set_ci_savedpc(ci, pc);
2096                        state.set_top(state.ci_top(ci));
2097                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
2098                            |a, b| imod(a, b), fmodf)?;
2099                    }
2100                    OpCode::Pow => {
2101                        let ra = base + i.arg_a();
2102                        let rb = base + i.arg_b();
2103                        let rc = base + i.arg_c();
2104                        if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
2105                            pc += 1;
2106                            let r = if n2 == 2.0 { n1 * n1 } else { n1.powf(n2) };
2107                            state.set_at(ra, LuaValue::Float(r));
2108                        }
2109                    }
2110                    OpCode::Div => {
2111                        let ra = base + i.arg_a();
2112                        let rb = base + i.arg_b();
2113                        let rc = base + i.arg_c();
2114                        if let Some((n1, n2)) = state.get_num_pair_at(rb, rc) {
2115                            pc += 1;
2116                            state.set_at(ra, LuaValue::Float(n1 / n2));
2117                        }
2118                    }
2119                    OpCode::IDiv => {
2120                        let ra = base + i.arg_a();
2121                        let v1 = state.get_at(base + i.arg_b());
2122                        let v2 = state.get_at(base + i.arg_c());
2123                        state.set_ci_savedpc(ci, pc);
2124                        state.set_top(state.ci_top(ci));
2125                        arith_op_checked(state, ra, &v1, &v2, &mut pc,
2126                            |a, b| idiv(a, b), |a, b| (a / b).floor())?;
2127                    }
2128                    // ── Bitwise with register operands ─────────────────────────
2129                    // if (tointegerns(v1, &i1) && tointegerns(v2, &i2)) { pc++; setivalue... }
2130                    OpCode::BAnd => {
2131                        let ra = base + i.arg_a();
2132                        let v1 = state.get_at(base + i.arg_b());
2133                        let v2 = state.get_at(base + i.arg_c());
2134                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_band);
2135                    }
2136                    OpCode::BOr => {
2137                        let ra = base + i.arg_a();
2138                        let v1 = state.get_at(base + i.arg_b());
2139                        let v2 = state.get_at(base + i.arg_c());
2140                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_bor);
2141                    }
2142                    OpCode::BXOr => {
2143                        let ra = base + i.arg_a();
2144                        let v1 = state.get_at(base + i.arg_b());
2145                        let v2 = state.get_at(base + i.arg_c());
2146                        bitwise_op_rr(state, ra, &v1, &v2, &mut pc, intop_bxor);
2147                    }
2148                    OpCode::Shr => {
2149                        let ra = base + i.arg_a();
2150                        let v1 = state.get_at(base + i.arg_b());
2151                        let v2 = state.get_at(base + i.arg_c());
2152                        bitwise_shift_rr(state, ra, &v1, &v2, &mut pc, true);
2153                    }
2154                    OpCode::Shl => {
2155                        let ra = base + i.arg_a();
2156                        let v1 = state.get_at(base + i.arg_b());
2157                        let v2 = state.get_at(base + i.arg_c());
2158                        bitwise_shift_rr(state, ra, &v1, &v2, &mut pc, false);
2159                    }
2160                    // ── OP_MMBIN ─────────────────────────────────────────────
2161                    // Instruction pi = *(pc - 2); TMS tm = (TMS)GETARG_C(i);
2162                    // StkId result = RA(pi);
2163                    // Protect(luaT_trybinTM(L, s2v(ra), rb, result, tm));
2164                    OpCode::MmBin => {
2165                        let ra_idx = base + i.arg_a();
2166                        let rb_idx = base + i.arg_b();
2167                        let ra_v = state.get_at(ra_idx);
2168                        let rb_v = state.get_at(rb_idx);
2169                        let tm = tagmethod_from_index(i.arg_c() as usize);
2170                        let prev_inst = state.proto_code(&cl, pc - 2);
2171                        let result_idx = base + prev_inst.arg_a();
2172                        state.set_ci_savedpc(ci, pc);
2173                        state.set_top(state.ci_top(ci));
2174                        state.try_bin_tm(&ra_v, Some(ra_idx), &rb_v, Some(rb_idx), result_idx, tm)?;
2175                        trap = state.ci_trap(ci);
2176                    }
2177                    OpCode::MmBinI => {
2178                        let ra_idx = base + i.arg_a();
2179                        let ra_v = state.get_at(ra_idx);
2180                        let imm = i.arg_s_b() as i64;
2181                        let tm = tagmethod_from_index(i.arg_c() as usize);
2182                        let flip = i.arg_k() != 0;
2183                        let prev_inst = state.proto_code(&cl, pc - 2);
2184                        let result_idx = base + prev_inst.arg_a();
2185                        state.set_ci_savedpc(ci, pc);
2186                        state.set_top(state.ci_top(ci));
2187                        state.try_bin_i_tm(&ra_v, Some(ra_idx), imm, flip, result_idx, tm)?;
2188                        trap = state.ci_trap(ci);
2189                    }
2190                    OpCode::MmBinK => {
2191                        let ra_idx = base + i.arg_a();
2192                        let ra_v = state.get_at(ra_idx);
2193                        let imm = state.proto_const(&cl, i.arg_b() as usize).clone();
2194                        let tm = tagmethod_from_index(i.arg_c() as usize);
2195                        let flip = i.arg_k() != 0;
2196                        let prev_inst = state.proto_code(&cl, pc - 2);
2197                        let result_idx = base + prev_inst.arg_a();
2198                        state.set_ci_savedpc(ci, pc);
2199                        state.set_top(state.ci_top(ci));
2200                        state.try_bin_assoc_tm(&ra_v, Some(ra_idx), &imm, None, flip, result_idx, tm)?;
2201                        trap = state.ci_trap(ci);
2202                    }
2203                    // ── OP_UNM ───────────────────────────────────────────────
2204                    //    else if (tonumberns(rb, nb)) setfltvalue(s2v(ra), -nb)
2205                    //    else Protect(luaT_trybinTM(L, rb, rb, ra, TM_UNM))
2206                    OpCode::Unm => {
2207                        let ra = base + i.arg_a();
2208                        let rb_idx = base + i.arg_b();
2209                        let rb_v = state.get_at(rb_idx);
2210                        match &rb_v {
2211                            LuaValue::Int(ib) => {
2212                                state.set_at(ra, LuaValue::Int(intop_sub(0, *ib)));
2213                            }
2214                            LuaValue::Float(nb) => {
2215                                state.set_at(ra, LuaValue::Float(-nb));
2216                            }
2217                            _ => {
2218                                state.set_ci_savedpc(ci, pc);
2219                                state.set_top(state.ci_top(ci));
2220                                state.try_bin_tm(&rb_v, Some(rb_idx), &rb_v, Some(rb_idx), ra, TagMethod::Unm)?;
2221                                trap = state.ci_trap(ci);
2222                            }
2223                        }
2224                    }
2225                    // ── OP_BNOT ──────────────────────────────────────────────
2226                    OpCode::BNot => {
2227                        let ra = base + i.arg_a();
2228                        let rb_idx = base + i.arg_b();
2229                        let rb_v = state.get_at(rb_idx);
2230                        if let Some(ib) = to_integer_ns(&rb_v, F2Imod::Eq) {
2231                            state.set_at(ra, LuaValue::Int(!ib));
2232                        } else {
2233                            state.set_ci_savedpc(ci, pc);
2234                            state.set_top(state.ci_top(ci));
2235                            state.try_bin_tm(&rb_v, Some(rb_idx), &rb_v, Some(rb_idx), ra, TagMethod::Bnot)?;
2236                            trap = state.ci_trap(ci);
2237                        }
2238                    }
2239                    // ── OP_NOT ───────────────────────────────────────────────
2240                    OpCode::Not => {
2241                        let ra = base + i.arg_a();
2242                        let rb_v = state.get_at(base + i.arg_b());
2243                        let falsy = matches!(rb_v, LuaValue::Nil | LuaValue::Bool(false));
2244                        state.set_at(ra, LuaValue::Bool(falsy));
2245                    }
2246                    // ── OP_LEN ───────────────────────────────────────────────
2247                    OpCode::Len => {
2248                        let ra = base + i.arg_a();
2249                        let rb_idx = base + i.arg_b();
2250                        let rb_v = state.get_at(rb_idx);
2251                        state.set_ci_savedpc(ci, pc);
2252                        state.set_top(state.ci_top(ci));
2253                        obj_len(state, ra, rb_v, rb_idx)?;
2254                        trap = state.ci_trap(ci);
2255                    }
2256                    // ── OP_CONCAT ─────────────────────────────────────────────
2257                    OpCode::Concat => {
2258                        let ra = base + i.arg_a();
2259                        let n = i.arg_b() as i32;
2260                        state.set_top(ra + n as i32);
2261                        state.set_ci_savedpc(ci, pc); // ProtectNT: save pc only
2262                        concat(state, n)?;
2263                        let top = state.top_idx();
2264                        state.set_ci_savedpc(ci, pc);
2265                        state.set_top(top);
2266                        state.gc_cond_step();
2267                        trap = state.ci_trap(ci);
2268                    }
2269                    // ── OP_CLOSE ──────────────────────────────────────────────
2270                    OpCode::Close => {
2271                        let ra = base + i.arg_a();
2272                        state.set_ci_savedpc(ci, pc);
2273                        state.set_top(state.ci_top(ci));
2274                        crate::func::close(state, ra, lua_types::status::LuaStatus::Ok as i32, true)?;
2275                        trap = state.ci_trap(ci);
2276                    }
2277                    // ── OP_TBC ────────────────────────────────────────────────
2278                    OpCode::Tbc => {
2279                        let ra = base + i.arg_a();
2280                        state.set_ci_savedpc(ci, pc);
2281                        state.set_top(state.ci_top(ci));
2282                        state.new_tbc_upval(ra)?;
2283                    }
2284                    // ── OP_JMP ────────────────────────────────────────────────
2285                    OpCode::Jmp => {
2286                        pc = (pc as i64 + i.arg_s_j() as i64) as u32;
2287                        trap = state.ci_trap(ci);
2288                    }
2289                    // ── OP_EQ ─────────────────────────────────────────────────
2290                    OpCode::Eq => {
2291                        let ra_v = state.get_at(base + i.arg_a());
2292                        let rb_v = state.get_at(base + i.arg_b());
2293                        state.set_ci_savedpc(ci, pc);
2294                        state.set_top(state.ci_top(ci));
2295                        let cond = equal_obj(Some(state), &ra_v, &rb_v)? as u32;
2296                        trap = state.ci_trap(ci);
2297                        if (cond as i32) != i.arg_k() {
2298                            pc += 1;
2299                        } else {
2300                            let next = state.proto_code(&cl, pc);
2301                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2302                            trap = state.ci_trap(ci);
2303                        }
2304                    }
2305                    // ── OP_LT ─────────────────────────────────────────────────
2306                    OpCode::Lt => {
2307                        let ra_v = state.get_at(base + i.arg_a());
2308                        let rb_v = state.get_at(base + i.arg_b());
2309                        let cond = if let (LuaValue::Int(ia), LuaValue::Int(ib)) = (&ra_v, &rb_v) {
2310                            *ia < *ib
2311                        } else if matches!((&ra_v, &rb_v),
2312                            (LuaValue::Int(_) | LuaValue::Float(_),
2313                             LuaValue::Int(_) | LuaValue::Float(_))) {
2314                            lt_num(&ra_v, &rb_v)
2315                        } else {
2316                            state.set_ci_savedpc(ci, pc);
2317                            state.set_top(state.ci_top(ci));
2318                            let r = less_than_others(state, &ra_v, &rb_v)?;
2319                            trap = state.ci_trap(ci);
2320                            r
2321                        };
2322                        if (cond as i32) != i.arg_k() {
2323                            pc += 1;
2324                        } else {
2325                            let next = state.proto_code(&cl, pc);
2326                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2327                            trap = state.ci_trap(ci);
2328                        }
2329                    }
2330                    // ── OP_LE ─────────────────────────────────────────────────
2331                    OpCode::Le => {
2332                        let ra_v = state.get_at(base + i.arg_a());
2333                        let rb_v = state.get_at(base + i.arg_b());
2334                        let cond = if let (LuaValue::Int(ia), LuaValue::Int(ib)) = (&ra_v, &rb_v) {
2335                            *ia <= *ib
2336                        } else if matches!((&ra_v, &rb_v),
2337                            (LuaValue::Int(_) | LuaValue::Float(_),
2338                             LuaValue::Int(_) | LuaValue::Float(_))) {
2339                            le_num(&ra_v, &rb_v)
2340                        } else {
2341                            state.set_ci_savedpc(ci, pc);
2342                            state.set_top(state.ci_top(ci));
2343                            let r = less_equal_others(state, &ra_v, &rb_v)?;
2344                            trap = state.ci_trap(ci);
2345                            r
2346                        };
2347                        if (cond as i32) != i.arg_k() {
2348                            pc += 1;
2349                        } else {
2350                            let next = state.proto_code(&cl, pc);
2351                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2352                            trap = state.ci_trap(ci);
2353                        }
2354                    }
2355                    // ── OP_EQK ────────────────────────────────────────────────
2356                    OpCode::EqK => {
2357                        let ra_v = state.get_at(base + i.arg_a());
2358                        let rb_v = state.proto_const(&cl, i.arg_b() as usize).clone();
2359                        let cond = equal_obj(None, &ra_v, &rb_v)? as u32;
2360                        if (cond as i32) != i.arg_k() {
2361                            pc += 1;
2362                        } else {
2363                            let next = state.proto_code(&cl, pc);
2364                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2365                            trap = state.ci_trap(ci);
2366                        }
2367                    }
2368                    // ── OP_EQI ────────────────────────────────────────────────
2369                    //    if (ttisinteger) cond = ivalue == im
2370                    //    elif (ttisfloat) cond = numeq(fltvalue, cast_num(im))
2371                    //    else cond = 0
2372                    OpCode::EqI => {
2373                        let ra_v = state.get_at(base + i.arg_a());
2374                        let im = i.arg_s_b() as i64;
2375                        let cond: bool = match &ra_v {
2376                            LuaValue::Int(iv) => *iv == im,
2377                            LuaValue::Float(fv) => *fv == im as f64,
2378                            _ => false,
2379                        };
2380                        if (cond as i32) != i.arg_k() {
2381                            pc += 1;
2382                        } else {
2383                            let next = state.proto_code(&cl, pc);
2384                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2385                            trap = state.ci_trap(ci);
2386                        }
2387                    }
2388                    // ── OP_LTI / OP_LEI / OP_GTI / OP_GEI ───────────────────
2389                    //              inv=0/0/1/1, tm=TM_LT/TM_LE/TM_LT/TM_LE)
2390                    OpCode::LtI => {
2391                        let ra = base + i.arg_a();
2392                        let im = i.arg_s_b() as i64;
2393                        let fast_cond = match &state.stack[ra.0 as usize].val {
2394                            LuaValue::Int(ia) => Some(*ia < im),
2395                            LuaValue::Float(fa) => Some(*fa < im as f64),
2396                            _ => None,
2397                        };
2398                        let cond = match fast_cond {
2399                            Some(cond) => cond,
2400                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, false, TagMethod::Lt)?,
2401                        };
2402                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2403                    }
2404                    OpCode::LeI => {
2405                        let ra = base + i.arg_a();
2406                        let im = i.arg_s_b() as i64;
2407                        let fast_cond = match &state.stack[ra.0 as usize].val {
2408                            LuaValue::Int(ia) => Some(*ia <= im),
2409                            LuaValue::Float(fa) => Some(*fa <= im as f64),
2410                            _ => None,
2411                        };
2412                        let cond = match fast_cond {
2413                            Some(cond) => cond,
2414                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, false, TagMethod::Le)?,
2415                        };
2416                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2417                    }
2418                    OpCode::GtI => {
2419                        let ra = base + i.arg_a();
2420                        let im = i.arg_s_b() as i64;
2421                        let fast_cond = match &state.stack[ra.0 as usize].val {
2422                            LuaValue::Int(ia) => Some(*ia > im),
2423                            LuaValue::Float(fa) => Some(*fa > im as f64),
2424                            _ => None,
2425                        };
2426                        let cond = match fast_cond {
2427                            Some(cond) => cond,
2428                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, true, TagMethod::Lt)?,
2429                        };
2430                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2431                    }
2432                    OpCode::GeI => {
2433                        let ra = base + i.arg_a();
2434                        let im = i.arg_s_b() as i64;
2435                        let fast_cond = match &state.stack[ra.0 as usize].val {
2436                            LuaValue::Int(ia) => Some(*ia >= im),
2437                            LuaValue::Float(fa) => Some(*fa >= im as f64),
2438                            _ => None,
2439                        };
2440                        let cond = match fast_cond {
2441                            Some(cond) => cond,
2442                            None => order_imm_slow(state, ra, pc, &mut trap, ci, i, im, true, TagMethod::Le)?,
2443                        };
2444                        finish_order_imm_jump(state, &cl, &mut pc, &mut trap, ci, i, cond);
2445                    }
2446                    // ── OP_TEST ────────────────────────────────────────────────
2447                    OpCode::Test => {
2448                        let ra_v = state.get_at(base + i.arg_a());
2449                        let cond = !matches!(ra_v, LuaValue::Nil | LuaValue::Bool(false));
2450                        if (cond as i32) != i.arg_k() {
2451                            pc += 1;
2452                        } else {
2453                            let next = state.proto_code(&cl, pc);
2454                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2455                            trap = state.ci_trap(ci);
2456                        }
2457                    }
2458                    // ── OP_TESTSET ─────────────────────────────────────────────
2459                    //    else { setobj2s(L, ra, rb); donextjump(ci); }
2460                    OpCode::TestSet => {
2461                        let ra = base + i.arg_a();
2462                        let rb_v = state.get_at(base + i.arg_b());
2463                        let falsy = matches!(rb_v, LuaValue::Nil | LuaValue::Bool(false));
2464                        if (falsy as i32) == i.arg_k() {
2465                            pc += 1;
2466                        } else {
2467                            state.set_at(ra, rb_v);
2468                            let next = state.proto_code(&cl, pc);
2469                            pc = (pc as i64 + next.arg_s_j() as i64 + 1) as u32;
2470                            trap = state.ci_trap(ci);
2471                        }
2472                    }
2473                    // ── OP_CALL ────────────────────────────────────────────────
2474                    //      updatetrap(ci);
2475                    //    else { ci = newci; goto startfunc; }
2476                    OpCode::Call => {
2477                        let ra = base + i.arg_a();
2478                        let b = i.arg_b();
2479                        let nresults = i.arg_c() as i32 - 1;
2480                        if b != 0 {
2481                            state.set_top(ra + b);
2482                        }
2483                        state.set_ci_savedpc(ci, pc); // savepc
2484                        let had_hook = state.hookmask != 0;
2485                        match state.precall(ra, nresults)? {
2486                            None => {
2487                                // C functions such as debug.sethook can change
2488                                // hook state during the call, so refresh the VM
2489                                // trap when hooks were or became relevant.
2490                                if had_hook || state.hookmask != 0 {
2491                                    trap = state.ci_trap(ci); // updatetrap
2492                                }
2493                            }
2494                            Some(new_ci) => {
2495                                // Lua function — goto startfunc
2496                                ci = new_ci;
2497                                continue 'startfunc;
2498                            }
2499                        }
2500                    }
2501                    // ── OP_TAILCALL ────────────────────────────────────────────
2502                    //      goto startfunc;
2503                    //    else { ci->func.p -= delta; luaD_poscall(L, ci, n);
2504                    //            updatetrap; goto ret; }
2505                    OpCode::TailCall => {
2506                        let ra = base + i.arg_a();
2507                        let b = i.arg_b();
2508                        let nparams1 = i.arg_c();
2509                        let delta = if nparams1 != 0 {
2510                            state.ci_nextraargs(ci) + nparams1 as i32
2511                        } else {
2512                            0
2513                        };
2514                        let top_b: i32 = if b != 0 {
2515                            state.set_top(ra + b);
2516                            b
2517                        } else {
2518                            state.top_idx() - ra
2519                        };
2520                        state.set_ci_savedpc(ci, pc);
2521                        if i.test_k() {
2522                            state.close_upvals_from_base(ci)?;
2523                        }
2524                        let n = state.pretailcall(ci, ra, top_b, delta)?;
2525                        if n < 0 {
2526                            // Lua function — goto startfunc
2527                            continue 'startfunc;
2528                        } else {
2529                            // C function — ci->func.p -= delta; luaD_poscall; goto ret
2530                            state.ci_adjust_func(ci, delta);
2531                            state.poscall(ci, n as u32)?;
2532                            if state.hookmask != 0 {
2533                                trap = state.ci_trap(ci);
2534                            }
2535                            break 'dispatch; // goto ret
2536                        }
2537                    }
2538                    // ── OP_RETURN ──────────────────────────────────────────────
2539                    //    savepc; if TESTARG_k: close upvals;
2540                    //    if nparams1: ci->func -= nextraargs+nparams1;
2541                    //    L->top.p = ra+n; luaD_poscall; goto ret
2542                    OpCode::Return => {
2543                        let ra = base + i.arg_a();
2544                        let n_raw = i.arg_b() as i32 - 1;
2545                        let nparams1 = i.arg_c();
2546                        let n: u32 = if n_raw < 0 {
2547                            (state.top_idx() - ra) as u32
2548                        } else {
2549                            n_raw as u32
2550                        };
2551                        state.set_ci_savedpc(ci, pc);
2552                        if i.test_k() {
2553                            state.ci_nres_set(ci, n as i32);
2554                            let ci_top = state.ci_top(ci);
2555                            if state.top_idx().0 < ci_top.0 {
2556                                state.set_top(ci_top);
2557                            }
2558                            crate::func::close(state, base, crate::func::CLOSE_K_TOP, true)?;
2559                            if state.hookmask != 0 {
2560                                trap = state.ci_trap(ci);
2561                            }
2562                            base = state.ci_base(ci); // updatestack
2563                        }
2564                        if nparams1 != 0 {
2565                            let nextraargs = state.ci_nextraargs(ci) as u32;
2566                            state.ci_adjust_func(ci, nextraargs as i32 + nparams1 as i32);
2567                        }
2568                        state.set_top(ra + n as i32);
2569                        state.poscall(ci, n)?;
2570                        if state.hookmask != 0 {
2571                            trap = state.ci_trap(ci);
2572                        }
2573                        break 'dispatch; // goto ret
2574                    }
2575                    // ── OP_RETURN0 ─────────────────────────────────────────────
2576                    //    else { L->ci = ci->previous; L->top = base-1;
2577                    //           for (nres = ci->nresults; nres > 0; nres--)
2578                    //             setnilvalue(L->top++) }
2579                    //    goto ret;
2580                    OpCode::Return0 => {
2581                        if state.hookmask == 0 {
2582                            let ci_slot = ci.as_usize();
2583                            let nres = state.call_info[ci_slot].nresults as i32;
2584                            state.ci = state.call_info[ci_slot]
2585                                .previous
2586                                .expect("RETURN0: returning frame has no previous CallInfo");
2587                            state.top = base - 1;
2588                            for _ in 0..nres.max(0) {
2589                                state.push(LuaValue::Nil);
2590                            }
2591                        } else {
2592                            return0_hook(state, ci, base, i, pc, &mut trap)?;
2593                        }
2594                        break 'dispatch; // goto ret
2595                    }
2596                    // ── OP_RETURN1 ─────────────────────────────────────────────
2597                    //    else { nres = ci->nresults; ci = ci->previous; ...handle results... }
2598                    //    goto ret;
2599                    OpCode::Return1 => {
2600                        if state.hookmask == 0 {
2601                            let ci_slot = ci.as_usize();
2602                            let nres = state.call_info[ci_slot].nresults as i32;
2603                            state.ci = state.call_info[ci_slot]
2604                                .previous
2605                                .expect("RETURN1: returning frame has no previous CallInfo");
2606                            if nres == 0 {
2607                                state.top = base - 1;
2608                            } else {
2609                                let ra = base + i.arg_a();
2610                                state.stack[(base - 1).0 as usize].val =
2611                                    state.stack[ra.0 as usize].val; // at least this result
2612                                state.top = base;
2613                                for _ in 1..nres.max(0) {
2614                                    state.push(LuaValue::Nil);
2615                                }
2616                            }
2617                        } else {
2618                            return1_hook(state, ci, base, i, pc, &mut trap)?;
2619                        }
2620                        break 'dispatch; // goto ret
2621                    }
2622                    // ── OP_FORLOOP ─────────────────────────────────────────────
2623                    //    else if (floatforloop(ra)) pc -= GETARG_Bx(i)
2624                    //    updatetrap(ci);
2625                    OpCode::ForLoop => {
2626                        let ra = base + i.arg_a();
2627                        let ra_u = ra.0 as usize;
2628                        if let LuaValue::Int(step) = state.stack[ra_u + 2].val {
2629                            let count = match state.stack[ra_u + 1].val {
2630                                LuaValue::Int(c) => c as u64,
2631                                _ => 0,
2632                            };
2633                            if count > 0 {
2634                                let idx = match state.stack[ra_u].val {
2635                                    LuaValue::Int(x) => x,
2636                                    _ => 0,
2637                                };
2638                                state.stack[ra_u + 1].val = LuaValue::Int((count - 1) as i64);
2639                                let new_idx = intop_add(idx, step);
2640                                state.stack[ra_u].val = LuaValue::Int(new_idx);
2641                                state.stack[ra_u + 3].val = LuaValue::Int(new_idx);
2642                                pc = (pc as i64 - i.arg_bx() as i64) as u32;
2643                            }
2644                        } else if float_for_loop(state, ra) {
2645                            pc = (pc as i64 - i.arg_bx() as i64) as u32;
2646                        }
2647                        trap = state.ci_trap(ci);
2648                    }
2649                    // ── OP_FORPREP ─────────────────────────────────────────────
2650                    OpCode::ForPrep => {
2651                        let ra = base + i.arg_a();
2652                        state.set_ci_savedpc(ci, pc);
2653                        state.set_top(state.ci_top(ci));
2654                        if forprep(state, ra)? {
2655                            pc = (pc as i64 + i.arg_bx() as i64 + 1) as u32;
2656                        }
2657                    }
2658                    // ── OP_TFORPREP ────────────────────────────────────────────
2659                    //    pc += GETARG_Bx(i); i = *pc++; assert(OP_TFORCALL && ra==RA(i));
2660                    //    goto l_tforcall;
2661                    OpCode::TForPrep => {
2662                        let ra = base + i.arg_a();
2663                        state.set_ci_savedpc(ci, pc);
2664                        state.set_top(state.ci_top(ci));
2665                        state.new_tbc_upval(ra + 3)?;
2666                        pc = (pc as i64 + i.arg_bx() as i64) as u32;
2667                        let tfc_i = state.proto_code(&cl, pc);
2668                        pc += 1;
2669                        debug_assert!(tfc_i.opcode() == OpCode::TForCall);
2670                        // inline l_tforcall:
2671                        let tfc_ra = base + tfc_i.arg_a();
2672                        for k in 0..3u32 {
2673                            let v = state.get_at(tfc_ra + k as i32);
2674                            state.set_at(tfc_ra + 4 + k as i32, v);
2675                        }
2676                        state.set_top(tfc_ra + 4 + 3);
2677                        state.set_ci_savedpc(ci, pc);
2678                        state.call_at(tfc_ra + 4, tfc_i.arg_c() as i32)?;
2679                        trap = state.ci_trap(ci);
2680                        base = state.ci_base(ci); // updatestack
2681                        let tfl_i = state.proto_code(&cl, pc);
2682                        pc += 1;
2683                        debug_assert!(tfl_i.opcode() == OpCode::TForLoop);
2684                        let tfl_ra = base + tfl_i.arg_a();
2685                        // inline l_tforloop:
2686                        if !matches!(state.get_at(tfl_ra + 4), LuaValue::Nil) {
2687                            let v = state.get_at(tfl_ra + 4);
2688                            state.set_at(tfl_ra + 2, v);
2689                            pc = (pc as i64 - tfl_i.arg_bx() as i64) as u32;
2690                        }
2691                    }
2692                    // ── OP_TFORCALL ────────────────────────────────────────────
2693                    OpCode::TForCall => {
2694                        let ra = base + i.arg_a();
2695                        for k in 0..3u32 {
2696                            let v = state.get_at(ra + k as i32);
2697                            state.set_at(ra + 4 + k as i32, v);
2698                        }
2699                        state.set_top(ra + 4 + 3);
2700                        state.set_ci_savedpc(ci, pc);
2701                        state.call_at(ra + 4, i.arg_c() as i32)?;
2702                        trap = state.ci_trap(ci);
2703                        base = state.ci_base(ci); // updatestack
2704                        let tfl_i = state.proto_code(&cl, pc);
2705                        pc += 1;
2706                        debug_assert!(tfl_i.opcode() == OpCode::TForLoop);
2707                        let tfl_ra = base + tfl_i.arg_a();
2708                        if !matches!(state.get_at(tfl_ra + 4), LuaValue::Nil) {
2709                            let v = state.get_at(tfl_ra + 4);
2710                            state.set_at(tfl_ra + 2, v);
2711                            pc = (pc as i64 - tfl_i.arg_bx() as i64) as u32;
2712                        }
2713                    }
2714                    // ── OP_TFORLOOP ────────────────────────────────────────────
2715                    OpCode::TForLoop => {
2716                        let ra = base + i.arg_a();
2717                        if !matches!(state.get_at(ra + 4), LuaValue::Nil) {
2718                            let v = state.get_at(ra + 4);
2719                            state.set_at(ra + 2, v);
2720                            pc = (pc as i64 - i.arg_bx() as i64) as u32;
2721                        }
2722                    }
2723                    // ── OP_SETLIST ─────────────────────────────────────────────
2724                    //    if TESTARG_k: last += Ax * (MAXARG_C+1); pc++;
2725                    //    for (; n > 0; n--) h->array[last-1] = val; luaC_barrierback
2726                    OpCode::SetList => {
2727                        let ra = base + i.arg_a();
2728                        let n_raw = i.arg_b();
2729                        let mut last = i.arg_c();
2730                        let t_val = state.get_at(ra);
2731                        let n: i32 = if n_raw == 0 {
2732                            state.top_idx() - ra - 1
2733                        } else {
2734                            state.set_top(state.ci_top(ci));
2735                            n_raw
2736                        };
2737                        last += n;
2738                        if i.test_k() {
2739                            let extra = state.proto_code(&cl, pc);
2740                            pc += 1;
2741                            const MAXARG_C: i32 = (1 << 8) - 1;
2742                            last += extra.arg_ax() * (MAXARG_C + 1);
2743                        }
2744                        state.table_ensure_array(&t_val, last as usize)?;
2745                        for k in (1..=n).rev() {
2746                            let val = state.get_at(ra + k as i32);
2747                            state.table_array_set(&t_val, (last - 1) as usize, val.clone())?;
2748                            last -= 1;
2749                            state.gc_barrier_back(&t_val, &val);
2750                        }
2751                    }
2752                    // ── OP_CLOSURE ─────────────────────────────────────────────
2753                    //    halfProtect(pushclosure(L, p, cl->upvals, base, ra));
2754                    //    checkGC(L, ra+1);
2755                    OpCode::Closure => {
2756                        let ra = base + i.arg_a();
2757                        let proto_idx = i.arg_bx() as usize;
2758                        state.set_ci_savedpc(ci, pc);
2759                        state.set_top(state.ci_top(ci));
2760                        push_closure(state, proto_idx, ci, base, ra)?;
2761                        // checkGC
2762                        state.set_ci_savedpc(ci, pc);
2763                        state.set_top(ra + 1);
2764                        state.gc_cond_step();
2765                        trap = state.ci_trap(ci);
2766                    }
2767                    // ── OP_VARARG ──────────────────────────────────────────────
2768                    OpCode::VarArg => {
2769                        let ra = base + i.arg_a();
2770                        let n = i.arg_c() as i32 - 1;
2771                        state.set_ci_savedpc(ci, pc);
2772                        state.set_top(state.ci_top(ci));
2773                        state.get_varargs(ci, ra, n)?;
2774                        trap = state.ci_trap(ci);
2775                    }
2776                    // ── OP_VARARGPREP ──────────────────────────────────────────
2777                    //    if (trap) luaD_hookcall(L, ci); L->oldpc = 1;
2778                    //    updatebase(ci);
2779                    OpCode::VarArgPrep => {
2780                        let nparams = i.arg_a();
2781                        state.set_ci_savedpc(ci, pc);
2782                        state.adjust_varargs(ci, nparams, &cl)?;
2783                        trap = state.ci_trap(ci);
2784                        if trap {
2785                            state.hook_call(ci)?;
2786                            state.set_oldpc(1);
2787                        }
2788                        base = state.ci_base(ci);
2789                    }
2790                    // ── OP_EXTRAARG ────────────────────────────────────────────
2791                    OpCode::ExtraArg => {
2792                        debug_assert!(false, "OP_EXTRAARG executed directly");
2793                    }
2794                    // ── OP_ERRNNIL (Lua 5.5 global-already-defined guard) ──────
2795                    //    luaG_errnnil: if the global's current value is non-nil,
2796                    //    raise `global '<name>' already defined`. Bx == 0 → "?",
2797                    //    else Bx-1 indexes the constant table for the name.
2798                    OpCode::ErrNNil => {
2799                        let ra = base + i.arg_a();
2800                        if !matches!(state.get_at(ra), LuaValue::Nil) {
2801                            let bx = i.arg_bx();
2802                            let name: Vec<u8> = if bx == 0 {
2803                                b"?".to_vec()
2804                            } else {
2805                                match state.proto_const(&cl, (bx - 1) as usize) {
2806                                    LuaValue::Str(s) => s.as_bytes().to_vec(),
2807                                    _ => b"?".to_vec(),
2808                                }
2809                            };
2810                            let mut msg = Vec::with_capacity(name.len() + 24);
2811                            msg.extend_from_slice(b"global '");
2812                            msg.extend_from_slice(&name);
2813                            msg.extend_from_slice(b"' already defined");
2814                            state.set_ci_savedpc(ci, pc);
2815                            return Err(crate::debug::prefixed_runtime_pub(state, msg));
2816                        }
2817                    }
2818                    // ── OP_VARARGPACK (Lua 5.5 named varargs) ──────────────────
2819                    //    Pack the current frame's extra varargs into a fresh
2820                    //    table stored in register A. Mirrors `table.pack(...)`:
2821                    //    a 1-based sequence of all extra args plus an integer
2822                    //    `.n` field counting them (nil holes included). The
2823                    //    extra args were moved by VARARGPREP to the slots just
2824                    //    below `ci->func`, i.e. `ci_func - nextra .. ci_func-1`.
2825                    OpCode::VarArgPack => {
2826                        let ra = base + i.arg_a();
2827                        let nextra = state.ci_nextraargs(ci);
2828                        let ci_func: StackIdx = state.ci_base(ci) - 1;
2829                        let t = if nextra > 0 {
2830                            state.new_table_with_sizes(nextra as u32, 1)?
2831                        } else {
2832                            state.new_table()
2833                        };
2834                        for k in 0..nextra {
2835                            let src: StackIdx = ci_func - nextra as i32 + k as i32;
2836                            let val = state.get_at(src);
2837                            t.raw_set_int(state, (k + 1) as i64, val)?;
2838                        }
2839                        let n_key = state.intern_str(b"n")?;
2840                        t.raw_set(state, LuaValue::Str(n_key), LuaValue::Int(nextra as i64))?;
2841                        state.set_at(ra, LuaValue::Table(t));
2842                        state.set_ci_savedpc(ci, pc);
2843                        state.gc_cond_step();
2844                        if state.hookmask != 0 {
2845                            trap = state.ci_trap(ci);
2846                        }
2847                    }
2848                } // end match opcode
2849            } // end 'dispatch loop
2850
2851            // ── ret: label ──────────────────────────────────────────────────
2852            if state.ci_is_fresh(ci) {
2853                return Ok(());
2854            } else {
2855                ci = state.ci_previous(ci).expect("ci_previous: not fresh frame must have previous");
2856                continue 'returning;
2857            }
2858        } // end 'returning loop
2859    } // end 'startfunc loop
2860}
2861
2862// ─── Local opcode dispatch helpers ───────────────────────────────────────────
2863
2864#[inline(always)]
2865fn number_value(v: LuaValue) -> Option<f64> {
2866    match v {
2867        LuaValue::Float(f) => Some(f),
2868        LuaValue::Int(i) => Some(i as f64),
2869        _ => None,
2870    }
2871}
2872
2873/// Increments `pc` on success (the `pc++` in the C macros).
2874#[allow(dead_code)]
2875#[inline]
2876fn arith_op_aux_rr(
2877    state: &mut LuaState,
2878    ra: StackIdx,
2879    v1: &LuaValue,
2880    v2: &LuaValue,
2881    pc: &mut u32,
2882    iop: fn(i64, i64) -> i64,
2883    fop: fn(f64, f64) -> f64,
2884) {
2885    if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (v1, v2) {
2886        *pc += 1;
2887        state.set_at(ra, LuaValue::Int(iop(*i1, *i2)));
2888    } else {
2889        arith_float_aux(state, ra, v1, v2, pc, fop);
2890    }
2891}
2892
2893#[allow(dead_code)]
2894#[inline]
2895fn arith_float_aux(
2896    state: &mut LuaState,
2897    ra: StackIdx,
2898    v1: &LuaValue,
2899    v2: &LuaValue,
2900    pc: &mut u32,
2901    fop: fn(f64, f64) -> f64,
2902) {
2903    let n1 = match v1 {
2904        LuaValue::Float(f) => Some(*f),
2905        LuaValue::Int(i) => Some(*i as f64),
2906        _ => None,
2907    };
2908    let n2 = match v2 {
2909        LuaValue::Float(f) => Some(*f),
2910        LuaValue::Int(i) => Some(*i as f64),
2911        _ => None,
2912    };
2913    if let (Some(n1), Some(n2)) = (n1, n2) {
2914        *pc += 1;
2915        state.set_at(ra, LuaValue::Float(fop(n1, n2)));
2916    }
2917}
2918
2919#[allow(dead_code)]
2920#[inline]
2921fn arith_op_checked(
2922    state: &mut LuaState,
2923    ra: StackIdx,
2924    v1: &LuaValue,
2925    v2: &LuaValue,
2926    pc: &mut u32,
2927    iop: fn(i64, i64) -> Result<i64, LuaError>,
2928    fop: fn(f64, f64) -> f64,
2929) -> Result<(), LuaError> {
2930    if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (v1, v2) {
2931        *pc += 1;
2932        let result = iop(*i1, *i2).map_err(|e| match e {
2933            LuaError::Runtime(LuaValue::Str(s)) => {
2934                crate::debug::prefixed_runtime_pub(state, s.as_bytes().to_vec())
2935            }
2936            other => other,
2937        })?;
2938        state.set_at(ra, LuaValue::Int(result));
2939    } else {
2940        arith_float_aux(state, ra, v1, v2, pc, fop);
2941    }
2942    Ok(())
2943}
2944
2945#[allow(dead_code)]
2946#[inline]
2947fn bitwise_op_k(
2948    state: &mut LuaState,
2949    ra: StackIdx,
2950    v1: &LuaValue,
2951    v2: &LuaValue, // must be integer (K constant)
2952    pc: &mut u32,
2953    op: fn(i64, i64) -> i64,
2954) {
2955    let i2 = match v2 {
2956        LuaValue::Int(i) => *i,
2957        _ => return,
2958    };
2959    if let Some(i1) = to_integer_ns(v1, F2Imod::Eq) {
2960        *pc += 1;
2961        state.set_at(ra, LuaValue::Int(op(i1, i2)));
2962    }
2963}
2964
2965#[allow(dead_code)]
2966#[inline]
2967fn bitwise_op_rr(
2968    state: &mut LuaState,
2969    ra: StackIdx,
2970    v1: &LuaValue,
2971    v2: &LuaValue,
2972    pc: &mut u32,
2973    op: fn(i64, i64) -> i64,
2974) {
2975    if let (Some(i1), Some(i2)) = (
2976        to_integer_ns(v1, F2Imod::Eq),
2977        to_integer_ns(v2, F2Imod::Eq),
2978    ) {
2979        *pc += 1;
2980        state.set_at(ra, LuaValue::Int(op(i1, i2)));
2981    }
2982}
2983
2984/// `right = true` negates `y` for right-shift semantics.
2985#[allow(dead_code)]
2986#[inline]
2987fn bitwise_shift_rr(
2988    state: &mut LuaState,
2989    ra: StackIdx,
2990    v1: &LuaValue,
2991    v2: &LuaValue,
2992    pc: &mut u32,
2993    right: bool,
2994) {
2995    if let (Some(i1), Some(i2)) = (
2996        to_integer_ns(v1, F2Imod::Eq),
2997        to_integer_ns(v2, F2Imod::Eq),
2998    ) {
2999        let y = if right { intop_sub(0, i2) } else { i2 };
3000        *pc += 1;
3001        state.set_at(ra, LuaValue::Int(shiftl(i1, y)));
3002    }
3003}
3004
3005/// Cold half of C's `op_orderI` macro: only reached when the operand is not a
3006/// plain integer/float and a metamethod lookup may be needed.
3007#[cold]
3008#[inline(never)]
3009#[allow(clippy::too_many_arguments)]
3010fn order_imm_slow(
3011    state: &mut LuaState,
3012    ra: StackIdx,
3013    pc: u32,
3014    trap: &mut bool,
3015    ci: CallInfoIdx,
3016    i: Instruction,
3017    im: i64,
3018    inv: bool,
3019    tm: TagMethod,
3020) -> Result<bool, LuaError> {
3021    let ra_v = state.get_at(ra);
3022    let isf = i.arg_c() != 0;
3023    state.set_ci_savedpc(ci, pc);
3024    state.set_top(state.ci_top(ci));
3025    let r = state.call_order_i_tm(&ra_v, im, inv, isf, tm)?;
3026    *trap = state.ci_trap(ci);
3027    Ok(r)
3028}
3029
3030#[inline(always)]
3031fn finish_order_imm_jump(
3032    state: &mut LuaState,
3033    cl: &lua_types::GcRef<lua_types::LuaLClosure>,
3034    pc: &mut u32,
3035    trap: &mut bool,
3036    ci: CallInfoIdx,
3037    i: Instruction,
3038    cond: bool,
3039) {
3040    if (cond as i32) != i.arg_k() {
3041        *pc += 1;
3042    } else {
3043        let next = state.proto_code(&cl, *pc);
3044        *pc = (*pc as i64 + next.arg_s_j() as i64 + 1) as u32;
3045        *trap = state.ci_trap(ci);
3046    }
3047}
3048
3049#[cold]
3050#[inline(never)]
3051fn return0_hook(
3052    state: &mut LuaState,
3053    ci: CallInfoIdx,
3054    base: StackIdx,
3055    i: Instruction,
3056    pc: u32,
3057    trap: &mut bool,
3058) -> Result<(), LuaError> {
3059    let ra = base + i.arg_a();
3060    state.set_top(ra);
3061    state.set_ci_savedpc(ci, pc);
3062    state.poscall(ci, 0)?;
3063    *trap = true;
3064    Ok(())
3065}
3066
3067#[cold]
3068#[inline(never)]
3069fn return1_hook(
3070    state: &mut LuaState,
3071    ci: CallInfoIdx,
3072    base: StackIdx,
3073    i: Instruction,
3074    pc: u32,
3075    trap: &mut bool,
3076) -> Result<(), LuaError> {
3077    let ra = base + i.arg_a();
3078    state.set_top(ra + 1);
3079    state.set_ci_savedpc(ci, pc);
3080    state.poscall(ci, 1)?;
3081    *trap = true;
3082    Ok(())
3083}
3084
3085// ──────────────────────────────────────────────────────────────────────────
3086// PORT STATUS
3087//   source:        src/lvm.c  (1899 lines, 32 functions)
3088//   target_crate:  lua-vm
3089//   confidence:    medium
3090//   todos:         6
3091//   port_notes:    4
3092//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
3093//   notes:         All opcode handlers and helpers translated; LuaState methods
3094//                  referenced (fast_get, precall, poscall, etc.) are stubs that
3095//                  Phase B will land.  The execute() goto flow is modelled with
3096//                  labelled Rust loops ('startfunc/'returning/'dispatch).
3097//                  str_to_number is a stub pending luaO_str2num port (TODO #1).
3098//                  strcoll replaced with byte-lexicographic order (TODO #2).
3099//                  order_imm_op uses LuaValue as a stand-in for GcRef<LuaClosure>
3100//                  (TODO #3).  ClosureRef type alias not yet defined (TODO #4-6).
3101// ──────────────────────────────────────────────────────────────────────────