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