Skip to main content

lua_vm/
vm.rs

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