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