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