Skip to main content

lua_vm/
tagmethods.rs

1//! Tag methods (metamethods) — ported from `ltm.c` / `ltm.h`.
2//!
3//! Every metamethod name (`__index`, `__add`, …) is interned on `GlobalState`
4//! during `init()`.  Lookup helpers (`get_tm`, `get_tm_by_obj`) return
5//! `LuaValue::Nil` when no metamethod is present; callers check with
6//! `value.is_nil()` (the `notm` macro in C).
7
8use std::borrow::Cow;
9
10#[allow(unused_imports)]
11use crate::prelude::*;
12use crate::state::LuaState;
13use lua_types::{CallInfoIdx, GcRef, LuaError, LuaType, LuaValue, StackIdx};
14
15// ── TagMethod enum (from ltm.h `TMS`) ────────────────────────────────────────
16
17/// Metamethod selector; one variant per `__xxx` event, in ORDER TM.
18///
19/// The discriminant values are load-bearing: they index into
20/// `GlobalState.tmname` and are used as bit positions in `Table.flags`.
21/// Do **not** reorder without grepping ORDER TM / ORDER OP.
22///
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24#[repr(u8)]
25pub(crate) enum TagMethod {
26    Index = 0,
27    NewIndex,
28    Gc,
29    Mode,
30    Len,
31    Eq,
32    Add,
33    Sub,
34    Mul,
35    Mod,
36    Pow,
37    Div,
38    IDiv,
39    BAnd,
40    BOr,
41    BXor,
42    Shl,
43    Shr,
44    Unm,
45    BNot,
46    Lt,
47    Le,
48    Concat,
49    Call,
50    Close,
51    N,
52}
53
54impl TagMethod {
55    /// Convert a raw u8 discriminant to a `TagMethod`.
56    /// Returns `TagMethod::N` (sentinel) if `v >= TM_N`.
57    ///
58    /// PORT NOTE: reshaped for borrowck — C casts freely; Rust requires an explicit map.
59    pub(crate) fn from_u8(v: u8) -> Self {
60        match v {
61            0 => TagMethod::Index,
62            1 => TagMethod::NewIndex,
63            2 => TagMethod::Gc,
64            3 => TagMethod::Mode,
65            4 => TagMethod::Len,
66            5 => TagMethod::Eq,
67            6 => TagMethod::Add,
68            7 => TagMethod::Sub,
69            8 => TagMethod::Mul,
70            9 => TagMethod::Mod,
71            10 => TagMethod::Pow,
72            11 => TagMethod::Div,
73            12 => TagMethod::IDiv,
74            13 => TagMethod::BAnd,
75            14 => TagMethod::BOr,
76            15 => TagMethod::BXor,
77            16 => TagMethod::Shl,
78            17 => TagMethod::Shr,
79            18 => TagMethod::Unm,
80            19 => TagMethod::BNot,
81            20 => TagMethod::Lt,
82            21 => TagMethod::Le,
83            22 => TagMethod::Concat,
84            23 => TagMethod::Call,
85            24 => TagMethod::Close,
86            _ => TagMethod::N,
87        }
88    }
89}
90
91/// Number of real metamethods (= `TagMethod::N as usize`).
92///
93pub(crate) const TM_N: usize = TagMethod::N as usize;
94
95// ── Type-name table (from ltm.h / ltm.c `luaT_typenames_`) ──────────────────
96
97//
98//   "no value",
99//   "nil", "boolean", udatatypename, "number",
100//   "string", "table", "function", udatatypename, "thread",
101//   "upvalue", "proto"
102// };
103//
104// Indexed as `luaT_typenames_[(x) + 1]` where x is the raw LuaType integer
105// (LUA_TNONE = -1, LUA_TNIL = 0, …, LUA_TPROTO = 10).
106// LUA_TOTALTYPES = LUA_TPROTO + 2 = 12 entries.
107//
108// PORT NOTE: C uses `const char*`; Rust uses `&'static [u8]` throughout.
109pub(crate) static TYPE_NAMES: &[&[u8]] = &[
110    b"no value", // index 0 → LUA_TNONE  (-1 + 1)
111    b"nil",      // index 1 → LUA_TNIL   ( 0 + 1)
112    b"boolean",  // index 2 → LUA_TBOOLEAN
113    b"userdata", // index 3 → LUA_TLIGHTUSERDATA
114    b"number",   // index 4 → LUA_TNUMBER
115    b"string",   // index 5 → LUA_TSTRING
116    b"table",    // index 6 → LUA_TTABLE
117    b"function", // index 7 → LUA_TFUNCTION
118    b"userdata", // index 8 → LUA_TUSERDATA
119    b"thread",   // index 9 → LUA_TTHREAD
120    b"upvalue",  // index 10 → LUA_TUPVAL
121    b"proto",    // index 11 → LUA_TPROTO
122];
123
124/// Return the human-readable type name for a `LuaType`.
125///
126///
127/// Panics in debug builds if `t` is out of the expected range (shouldn't
128/// happen with a well-formed `LuaType`).
129pub(crate) fn type_name(t: LuaType) -> &'static [u8] {
130    let idx = (t as i32 + 1) as usize;
131    TYPE_NAMES.get(idx).copied().unwrap_or(b"?")
132}
133
134// ── luaT_init ────────────────────────────────────────────────────────────────
135
136/// Intern all metamethod name strings and pin them in the GC.
137///
138/// Must be called exactly once during `LuaState` initialization, before any
139/// metamethod lookup.  After this call, `GlobalState.tmname[i]` holds the
140/// interned `LuaString` for metamethod `i`.
141pub(crate) fn init(state: &mut LuaState) -> Result<(), LuaError> {
142    //     "__index", "__newindex",
143    //     "__gc", "__mode", "__len", "__eq",
144    //     "__add", "__sub", "__mul", "__mod", "__pow",
145    //     "__div", "__idiv",
146    //     "__band", "__bor", "__bxor", "__shl", "__shr",
147    //     "__unm", "__bnot", "__lt", "__le",
148    //     "__concat", "__call", "__close"
149    // };
150    const EVENT_NAMES: &[&[u8]] = &[
151        b"__index",
152        b"__newindex",
153        b"__gc",
154        b"__mode",
155        b"__len",
156        b"__eq",
157        b"__add",
158        b"__sub",
159        b"__mul",
160        b"__mod",
161        b"__pow",
162        b"__div",
163        b"__idiv",
164        b"__band",
165        b"__bor",
166        b"__bxor",
167        b"__shl",
168        b"__shr",
169        b"__unm",
170        b"__bnot",
171        b"__lt",
172        b"__le",
173        b"__concat",
174        b"__call",
175        b"__close",
176    ];
177    debug_assert!(EVENT_NAMES.len() == TM_N);
178
179    // PORT NOTE: The C `tmname[TM_N]` is a fixed-size array on `global_State`;
180    // the Rust port uses `Vec<GcRef<LuaString>>` initialized empty in
181    // `lua_state_init`, so we must grow it to `TM_N` before indexed assignment.
182    if state.global().tmname.len() < TM_N {
183        let pad = state.intern_str(b"")?;
184        state.global_mut().tmname.resize(TM_N, pad);
185    }
186
187    //     G(L)->tmname[i] = luaS_new(L, luaT_eventname[i]);
188    //     luaC_fix(L, obj2gco(G(L)->tmname[i]));  /* never collect these names */
189    // }
190    for (i, &name) in EVENT_NAMES.iter().enumerate() {
191        let interned = state.intern_str(name)?;
192        state.global_mut().tmname[i] = interned.clone();
193        // Pin the string so the GC never collects it.
194        // TODO(port): luaC_fix API on gc() is TBD; no-op in Phase A–C (Rc keeps it alive)
195        state.gc().fix_object(&interned);
196    }
197    Ok(())
198}
199
200// ── luaT_gettmbyobj ──────────────────────────────────────────────────────────
201
202/// Look up a metamethod for any Lua value by dispatching on its type.
203///
204/// Tables and full userdata have per-object metatables; all other types use
205/// the per-type metatables on `GlobalState`.  Returns `LuaValue::Nil` when
206/// neither the object nor its type has a metatable, or when the metatable
207/// does not contain the requested metamethod.
208pub(crate) fn get_tm_by_obj(state: &mut LuaState, o: &LuaValue, event: TagMethod) -> LuaValue {
209    //   case LUA_TTABLE:    mt = hvalue(o)->metatable; break;
210    //   case LUA_TUSERDATA: mt = uvalue(o)->metatable; break;
211    //   default:            mt = G(L)->mt[ttype(o)];
212    // }
213    //
214    // TODO(port): `GcRef<LuaTable>` access pattern (direct field vs borrow) is
215    // TBD pending the GcRef/RefCell decision in Phase B; using `.metatable()`
216    // accessor here as a placeholder.
217    let mt: Option<GcRef<lua_types::value::LuaTable>> = match o {
218        LuaValue::Table(t) => t.metatable(),
219        LuaValue::UserData(u) => u.metatable(),
220        _ => {
221            let type_idx = o.base_type() as usize;
222            state.global().mt[type_idx].clone()
223        }
224    };
225
226    match mt {
227        Some(mt_ref) => {
228            // Clone the name string before the table lookup to avoid borrow conflict.
229            let ename = state.global().tmname[event as usize].clone();
230            mt_ref.get_short_str(&ename)
231        }
232        None => LuaValue::Nil,
233    }
234}
235
236// ── luaT_objtypename ─────────────────────────────────────────────────────────
237
238/// Return the human-readable type name for a Lua value without any heap
239/// allocation in the common case.
240///
241/// For tables and full userdata whose metatable defines `__name` as a string,
242/// returns `Cow::Owned` with the custom name bytes.  For every other case
243/// returns `Cow::Borrowed` pointing into the static `TYPE_NAMES` table —
244/// no allocation, no interning, no `LuaState` access required.
245///
246/// PORT NOTE: C returns `const char*` — either a pointer into a GC-managed
247/// `LuaString` or a pointer into the static `luaT_typenames_` array.  Rust
248/// models this as `Cow<'static, [u8]>`: `Borrowed` for static names,
249/// `Owned` only when the metatable `__name` field overrides the default.
250/// Uses `LuaTable::get_str_bytes` (linear byte-scan) instead of
251/// `intern_str` + `get_short_str` so the lookup is infallible and requires
252/// no mutable state access.
253pub(crate) fn obj_type_name_cow(o: &LuaValue) -> Cow<'static, [u8]> {
254    if matches!(o, LuaValue::LightUserData(_)) {
255        return Cow::Borrowed(b"light userdata");
256    }
257    //        (ttisfulluserdata(o) && (mt = uvalue(o)->metatable) != NULL))
258    let mt: Option<GcRef<lua_types::value::LuaTable>> = match o {
259        LuaValue::Table(t) => t.metatable(),
260        LuaValue::UserData(u) => u.metatable(),
261        _ => None,
262    };
263    if let Some(mt_ref) = mt {
264        // Uses get_str_bytes (raw byte scan) rather than intern_str + get_short_str
265        // so no mutable state is needed and no error can propagate.
266        let name_val = mt_ref.get_str_bytes(b"__name");
267        if let LuaValue::Str(s) = name_val {
268            return Cow::Owned(s.as_bytes().to_vec());
269        }
270    }
271    Cow::Borrowed(type_name(o.base_type()))
272}
273
274/// Compatibility wrapper returning `Vec<u8>` for callers that have not yet
275/// migrated to `obj_type_name_cow`.  Always allocates; prefer
276/// `obj_type_name_cow` for allocation-free lookup in error-path code.
277///
278/// PORT NOTE: `state` parameter retained for API compatibility; it is no
279/// longer used since the implementation delegates to `obj_type_name_cow`.
280/// Fallibility (`Result`) is also retained for the same reason.
281pub(crate) fn obj_type_name(_state: &mut LuaState, o: &LuaValue) -> Result<Vec<u8>, LuaError> {
282    Ok(obj_type_name_cow(o).into_owned())
283}
284
285// ── luaT_callTM ──────────────────────────────────────────────────────────────
286
287//                      const TValue *p2, const TValue *p3)
288/// Call tag method `f` with three arguments, discarding any return values.
289///
290/// Used for metamethods like `__gc` and `__close` that take three operands
291/// and whose return value is irrelevant.
292///
293/// If the current call frame is Lua bytecode (`isLuacode`), the metamethod
294/// may yield; otherwise yielding is suppressed (`callnoyield`).
295pub(crate) fn call_tm(
296    state: &mut LuaState,
297    f: LuaValue,
298    p1: LuaValue,
299    p2: LuaValue,
300    p3: LuaValue,
301) -> Result<(), LuaError> {
302    let func = state.top_idx();
303    //
304    // PORT NOTE: In C these are direct writes into the EXTRA_STACK reserve
305    // area above the official top.  In Rust we use push() which manages
306    // capacity; the semantic result is identical.
307    state.push(f);
308    state.push(p1);
309    state.push(p2);
310    state.push(p3);
311    //      luaD_call(L, func, 0);
312    //    else
313    //      luaD_callnoyield(L, func, 0);
314    //
315    // TODO(port): `do_call(func, nresults)` vs `call_from(func, nresults)` —
316    // exact Rust API name TBD; nargs is implicit as `top - func - 1`.
317    if state.current_ci().is_lua_code() {
318        state.do_call(func, 0)?;
319    } else {
320        state.do_call_no_yield(func, 0)?;
321    }
322    Ok(())
323}
324
325// ── luaT_callTMres ───────────────────────────────────────────────────────────
326
327//                          const TValue *p2, StkId res)
328/// Call tag method `f` with two arguments, writing the single result into
329/// the stack slot at index `res`.
330///
331/// `res` is a `StackIdx` (index-stable across stack reallocation) that must
332/// refer to a pre-existing or scratch slot.  After return, the stack top is
333/// back to what it was before the call (i.e. `top == res`).
334///
335/// C uses `savestack`/`restorestack` (byte-offset from base) to preserve `res`
336/// across potential stack reallocations inside `luaD_call`.  In Rust, `StackIdx`
337/// is already an index and needs no save/restore.
338pub(crate) fn call_tm_res(
339    state: &mut LuaState,
340    f: LuaValue,
341    p1: LuaValue,
342    p2: LuaValue,
343    res: StackIdx,
344) -> Result<(), LuaError> {
345    // savestack → StackIdx is already the stable byte-offset analogue; no-op.
346
347    let func = state.top_idx();
348    state.push(f);
349    state.push(p1);
350    state.push(p2);
351
352    //      luaD_call(L, func, 1);
353    //    else
354    //      luaD_callnoyield(L, func, 1);
355    //
356    // TODO(port): same `do_call` API question as in call_tm above.
357    if state.current_ci().is_lua_code() {
358        state.do_call(func, 1)?;
359    } else {
360        state.do_call_no_yield(func, 1)?;
361    }
362
363    // restorestack → StackIdx is already stable; `res` is unchanged.
364
365    // Pre-decrement top, then copy that slot to res.
366    let result_val = state.pop();
367    state.set_at(res, result_val);
368    Ok(())
369}
370
371// ── callbinTM (static) ────────────────────────────────────────────────────────
372
373//                           StkId res, TMS event)
374/// Try to find and call a binary tag method for `event`.
375///
376/// Checks `p1` first, then `p2`, for a metamethod.  If neither has one,
377/// returns `false` and leaves `res` unmodified.  If found, calls it with
378/// `(p1, p2)` as arguments, writes the result to slot `res`, and returns `true`.
379fn call_bin_tm(
380    state: &mut LuaState,
381    p1: &LuaValue,
382    p2: &LuaValue,
383    res: StackIdx,
384    event: TagMethod,
385) -> Result<bool, LuaError> {
386    let tm = get_tm_by_obj(state, p1, event);
387    //      tm = luaT_gettmbyobj(L, p2, event);  /* try second operand */
388    let tm = if tm.is_nil() {
389        get_tm_by_obj(state, p2, event)
390    } else {
391        tm
392    };
393    if tm.is_nil() {
394        return Ok(false);
395    }
396    // Clone p1/p2 before the mutable borrow of `state` via call_tm_res.
397    call_tm_res(state, tm, p1.clone(), p2.clone(), res)?;
398    Ok(true)
399}
400
401/// Lua 5.3 core string→integer coercion for bitwise ops.
402///
403/// Returns:
404/// - `Some(Ok(()))` — both operands coerced to integers; the op was computed
405///   and written to `res`.
406/// - `Some(Err(..))` — an operand is a numeric-but-non-integral string
407///   (e.g. `"3.5"`, `"0xff..ff.0"`); raises "number has no integer
408///   representation" (`luaG_tointerror`), matching lua5.3.6.
409/// - `None` — coercion is impossible (an operand is a non-numeric string such
410///   as `"abc"`); the caller falls through to its normal error path, which
411///   raises "perform bitwise operation on".
412///
413/// The 5.3-only gate and the bitwise-event filter are applied by the caller.
414fn try_bitwise_strconv_53(
415    state: &mut LuaState,
416    p1: &LuaValue,
417    p1_idx: Option<StackIdx>,
418    p2: &LuaValue,
419    p2_idx: Option<StackIdx>,
420    res: StackIdx,
421    event: TagMethod,
422) -> Option<Result<(), LuaError>> {
423    // Both operands must be number-ish (integer, float, or a string that
424    // parses as a number). If either is genuinely non-numeric, bail to the
425    // caller's "perform bitwise operation on" path.
426    let n1 = p1.to_number_with_strconv();
427    let n2 = p2.to_number_with_strconv();
428    if n1.is_none() || n2.is_none() {
429        return None;
430    }
431    // Both are number-ish. Now require integer representations. If a number-ish
432    // operand has no integer representation (non-integral numeric string or
433    // float), 5.3 raises "number has no integer representation".
434    let i1 = p1.to_integer_with_strconv();
435    let i2 = p2.to_integer_with_strconv();
436    let (i1, i2) = match (i1, i2) {
437        (Some(a), Some(b)) => (a, b),
438        _ => {
439            let p1_idx = p1_idx.unwrap_or(StackIdx(0));
440            let p2_idx = p2_idx.unwrap_or(StackIdx(0));
441            return Some(Err(crate::debug::to_int_error(
442                state,
443                p1,
444                Some(p1_idx),
445                p2,
446                Some(p2_idx),
447            )));
448        }
449    };
450    let result = match event {
451        TagMethod::BAnd => i1 & i2,
452        TagMethod::BOr => i1 | i2,
453        TagMethod::BXor => i1 ^ i2,
454        TagMethod::Shl => crate::vm::shiftl(i1, i2),
455        TagMethod::Shr => crate::vm::shiftl(i1, i2.wrapping_neg()),
456        // Unary `~x` arrives here as a binary event with p1 == p2; `~i1`.
457        TagMethod::BNot => !i1,
458        _ => return None,
459    };
460    state.set_at(res, LuaValue::Int(result));
461    Some(Ok(()))
462}
463
464// ── luaT_trybinTM ────────────────────────────────────────────────────────────
465
466//                         StkId res, TMS event)
467/// Attempt a binary metamethod call; raise a type error if no metamethod exists.
468///
469/// For bitwise operations, further distinguishes between:
470/// - Both operands are numbers but not integers → `LuaError::int_overflow`
471///   (`luaG_tointerror` — "number has no integer representation")
472/// - At least one operand is not a number → `LuaError::arith_error` with
473///   "perform bitwise operation on"
474///
475/// All other missing metamethods raise `LuaError::arith_error` with
476/// "perform arithmetic on".
477pub(crate) fn try_bin_tm(
478    state: &mut LuaState,
479    p1: &LuaValue,
480    p1_idx: Option<StackIdx>,
481    p2: &LuaValue,
482    p2_idx: Option<StackIdx>,
483    res: StackIdx,
484    event: TagMethod,
485) -> Result<(), LuaError> {
486    // Lua 5.3 arith error wording with string operands. In the shared (5.4)
487    // model arithmetic metamethods (`__add`/`__sub`/…) are installed on the
488    // string metatable, so a failed arith fast path that has a string operand
489    // dispatches to the string-library `trymt`, which raises `attempt to <op> a
490    // '<t>' with a '<t>'`, adds a spurious `[C]: in metamethod '<op>'` frame, and
491    // cannot produce operand varinfo (it runs as a C function with no access to
492    // the calling bytecode registers). 5.3 instead owns arithmetic string
493    // coercion in the core: when the operation cannot succeed it raises `attempt
494    // to perform arithmetic on a <type> value (<varinfo>)`, blaming the operand
495    // that does not coerce to a number.
496    //
497    // The intercept is narrow: it fires ONLY when a string operand cannot be
498    // coerced to a number AND the other operand carries no genuine arith
499    // metamethod of its own. So `t + "5"` (t has `__add`) still dispatches to
500    // t's metamethod via `call_bin_tm`, and the coercible success path
501    // (`"3" + 2`) still flows through the string metamethod below, preserving
502    // 5.3 float-promotion semantics. See specs/followup/5.3-coerce-err.md.
503    if matches!(state.global().lua_version, lua_types::LuaVersion::V53)
504        && matches!(
505            event,
506            TagMethod::Add
507                | TagMethod::Sub
508                | TagMethod::Mul
509                | TagMethod::Mod
510                | TagMethod::Pow
511                | TagMethod::Div
512                | TagMethod::IDiv
513                | TagMethod::Unm
514        )
515        && (matches!(p1, LuaValue::Str(_)) || matches!(p2, LuaValue::Str(_)))
516    {
517        use crate::state::LuaValueExt;
518        let p1_num = p1.to_number_with_strconv().is_some();
519        let p2_num = p2.to_number_with_strconv().is_some();
520        if !(p1_num && p2_num) {
521            // A string operand did not coerce. Only raise the core error here if
522            // the non-string operand has no genuine arith metamethod; otherwise
523            // fall through so `call_bin_tm` dispatches to that real metamethod
524            // (the string metatable's synthetic arith mm is ignored for this
525            // decision — it never produces a useful result for a non-coercible
526            // pairing, it only raises the wrong-version wording).
527            //
528            // Unary minus arrives as a binary event with `p1 == p2`, so there is
529            // no genuine "other operand" — the only metamethod available is the
530            // synthetic string one. Treat it as absent so `-"x"` takes the core
531            // path.
532            let unary = matches!(event, TagMethod::Unm);
533            let other_has_mm = !unary
534                && if matches!(p1, LuaValue::Str(_)) {
535                    !get_tm_by_obj(state, p2, event).is_nil()
536                } else {
537                    !get_tm_by_obj(state, p1, event).is_nil()
538                };
539            if !other_has_mm {
540                // Point varinfo at the operand that does not coerce, matching C
541                // `luaG_opinterror`. A coercible numeric string counts as a
542                // number, so `'2' * nil` blames `nil`, not `'2'`.
543                let (bad, bad_idx) = if !p1_num {
544                    (p1, p1_idx.unwrap_or(StackIdx(0)))
545                } else {
546                    (p2, p2_idx.unwrap_or(StackIdx(0)))
547                };
548                return Err(crate::debug::type_error(
549                    state,
550                    bad,
551                    bad_idx,
552                    b"perform arithmetic on",
553                ));
554            }
555        }
556    }
557    if !call_bin_tm(state, p1, p2, res, event)? {
558        // Lua 5.3 coerces numeric strings to integers in the *core* bitwise
559        // ops (`& | ~ << >>` and unary `~`), where 5.4/5.5 require a real
560        // number operand and delegate string handling to a (non-existent)
561        // string metamethod. On the 5.3 path, after the metamethod lookup
562        // fails, retry the operation with string→integer coercion before
563        // raising. The boundary semantics (non-integral numeric string →
564        // "no integer representation"; non-numeric string → "perform bitwise
565        // operation") fall out of `to_integer_with_strconv` /
566        // `to_number_with_strconv`. See specs/followup/5.3-coerce-err.md.
567        if matches!(state.global().lua_version, lua_types::LuaVersion::V53)
568            && matches!(
569                event,
570                TagMethod::BAnd
571                    | TagMethod::BOr
572                    | TagMethod::BXor
573                    | TagMethod::Shl
574                    | TagMethod::Shr
575                    | TagMethod::BNot
576            )
577            && (matches!(p1, LuaValue::Str(_)) || matches!(p2, LuaValue::Str(_)))
578        {
579            if let Some(result) = try_bitwise_strconv_53(state, p1, p1_idx, p2, p2_idx, res, event)
580            {
581                return result;
582            }
583        }
584        //   case TM_BAND: case TM_BOR: case TM_BXOR:
585        //   case TM_SHL: case TM_SHR: case TM_BNOT: {
586        //     if (ttisnumber(p1) && ttisnumber(p2))
587        //       luaG_tointerror(L, p1, p2);
588        //     else
589        //       luaG_opinterror(L, p1, p2, "perform bitwise operation on");
590        //   }
591        //   /* calls never return, but to avoid warnings: *//* FALLTHROUGH */
592        //   default:
593        //     luaG_opinterror(L, p1, p2, "perform arithmetic on");
594        // }
595        //
596        // PORT NOTE: the C switch has a dead "FALLTHROUGH" for bitwise cases
597        // because both branches of the inner if/else call noreturn functions.
598        // In Rust `match` has no implicit fallthrough; each arm is self-contained
599        // and explicitly returns `Err(...)`.
600        match event {
601            TagMethod::BAnd
602            | TagMethod::BOr
603            | TagMethod::BXor
604            | TagMethod::Shl
605            | TagMethod::Shr
606            | TagMethod::BNot => {
607                if matches!(p1, LuaValue::Int(_) | LuaValue::Float(_))
608                    && matches!(p2, LuaValue::Int(_) | LuaValue::Float(_))
609                {
610                    // "(field 'huge')" / "(local 'x')" etc. based on the bytecode that
611                    // produced the bad operand.
612                    return Err(crate::debug::to_int_error(state, p1, p1_idx, p2, p2_idx));
613                } else {
614                    // varinfo on the non-number operand.
615                    let p1_idx = p1_idx.unwrap_or(StackIdx(0));
616                    let p2_idx = p2_idx.unwrap_or(StackIdx(0));
617                    return Err(crate::debug::op_int_error(
618                        state,
619                        p1,
620                        p1_idx,
621                        p2,
622                        p2_idx,
623                        b"perform bitwise operation on",
624                    ));
625                }
626            }
627            _ => {
628                // varinfo enriches with "(global 'aaa')" etc.
629                let p1_idx = p1_idx.unwrap_or(StackIdx(0));
630                let p2_idx = p2_idx.unwrap_or(StackIdx(0));
631                return Err(crate::debug::op_int_error(
632                    state,
633                    p1,
634                    p1_idx,
635                    p2,
636                    p2_idx,
637                    b"perform arithmetic on",
638                ));
639            }
640        }
641    }
642    Ok(())
643}
644
645// ── luaT_tryconcatTM ─────────────────────────────────────────────────────────
646
647/// Attempt the `__concat` metamethod on the two values at the top of the stack.
648///
649/// Reads `stack[top-2]` and `stack[top-1]`, searches both for `__concat`,
650/// calls it with `(stack[top-2], stack[top-1])` writing the result back to
651/// `stack[top-2]`, or raises `LuaError::concat_error` if no metamethod exists.
652pub(crate) fn try_concat_tm(state: &mut LuaState) -> Result<(), LuaError> {
653    let top = state.top_idx();
654    //      luaG_concaterror(L, s2v(top - 2), s2v(top - 1));
655    //
656    // Clone the operands before any call that might mutate the stack.
657    let p1 = state.get_at(top - 2).clone();
658    let p2 = state.get_at(top - 1).clone();
659    if !call_bin_tm(state, &p1, &p2, top - 2, TagMethod::Concat)? {
660        let p1_ok = matches!(p1, LuaValue::Str(_) | LuaValue::Int(_) | LuaValue::Float(_));
661        let (bad, bad_idx) = if p1_ok {
662            (&p2, top - 1)
663        } else {
664            (&p1, top - 2)
665        };
666        return Err(crate::debug::type_error(
667            state,
668            bad,
669            bad_idx,
670            b"concatenate",
671        ));
672    }
673    Ok(())
674}
675
676// ── luaT_trybinassocTM ───────────────────────────────────────────────────────
677
678//                              int flip, StkId res, TMS event)
679/// Try a binary associative metamethod, optionally swapping the operands.
680///
681/// When `flip` is `true`, operands are exchanged before dispatch.  This
682/// implements Lua's symmetry rule: if `a OP b` finds no metamethod on `a`,
683/// the VM retries with `b OP a` (setting flip=true to restore the original
684/// argument order for the call).
685pub(crate) fn try_bin_assoc_tm(
686    state: &mut LuaState,
687    p1: &LuaValue,
688    p1_idx: Option<StackIdx>,
689    p2: &LuaValue,
690    p2_idx: Option<StackIdx>,
691    flip: bool,
692    res: StackIdx,
693    event: TagMethod,
694) -> Result<(), LuaError> {
695    //      luaT_trybinTM(L, p2, p1, res, event);
696    //    else
697    //      luaT_trybinTM(L, p1, p2, res, event);
698    if flip {
699        try_bin_tm(state, p2, p2_idx, p1, p1_idx, res, event)
700    } else {
701        try_bin_tm(state, p1, p1_idx, p2, p2_idx, res, event)
702    }
703}
704
705// ── luaT_trybiniTM ───────────────────────────────────────────────────────────
706
707//                          int flip, StkId res, TMS event)
708/// Try a binary metamethod where the second operand is an integer constant.
709///
710/// Boxes `i2` as `LuaValue::Int` and delegates to `try_bin_assoc_tm`.
711pub(crate) fn try_bini_tm(
712    state: &mut LuaState,
713    p1: &LuaValue,
714    p1_idx: Option<StackIdx>,
715    i2: i64,
716    flip: bool,
717    res: StackIdx,
718    event: TagMethod,
719) -> Result<(), LuaError> {
720    let aux = LuaValue::Int(i2);
721    // The immediate operand has no stack location, so it gets `None`.
722    try_bin_assoc_tm(state, p1, p1_idx, &aux, None, flip, res, event)
723}
724
725// ── luaT_callorderTM ─────────────────────────────────────────────────────────
726
727//                           TMS event)
728/// Call an order metamethod (`__lt` or `__le`) and return its boolean result.
729///
730/// Returns `true` if the metamethod returned a truthy value.
731/// Raises `LuaError::order_error` if neither operand has the metamethod.
732///
733/// PORT NOTE: `LUA_COMPAT_LT_LE` (deriving `__le` from `__lt`) is ON by default
734/// in the reference `make macosx` builds of 5.1–5.4 and removed in 5.5. We match
735/// the default-built reference (the pinned oracle, per specs/oracle/CONTRACT.md),
736/// so the fallback is implemented and version-gated: derive for 5.1–5.4, raise
737/// for 5.5.
738pub(crate) fn call_order_tm(
739    state: &mut LuaState,
740    p1: &LuaValue,
741    p2: &LuaValue,
742    event: TagMethod,
743) -> Result<bool, LuaError> {
744    //      return !l_isfalse(s2v(L->top.p));
745    //
746    // PORT NOTE: In C, `L->top.p` is used as a scratch slot (written by
747    // callTMres then immediately read) in the EXTRA_STACK reserved area above
748    // the official stack top — the stack top is NOT officially advanced.
749    // In Rust we pass `state.top_idx()` as `res`; call_bin_tm → call_tm_res
750    // pushes 3 values, calls, pops the result back to res, and leaves top ==
751    // res (i.e. top unchanged relative to entry).  Reading get_at(res_idx)
752    // after this is safe because the slot was just written and top == res_idx.
753    //
754    // TODO(port): Verify in Phase B that no call path between call_bin_tm
755    // returning and the get_at read can disturb the scratch slot or reset top
756    // below res_idx.  The invariant holds as long as do_call with 1 result
757    // leaves exactly one value on the stack above func.
758    let res_idx = state.top_idx();
759    if call_bin_tm(state, p1, p2, res_idx, event)? {
760        // l_isfalse(o) → matches!(o, LuaValue::Nil | LuaValue::Bool(false))
761        let result = state.get_at(res_idx).clone();
762        return Ok(!matches!(result, LuaValue::Nil | LuaValue::Bool(false)));
763    }
764
765    // LUA_COMPAT_LT_LE: in 5.1–5.4 a missing `__le` falls back to `not (b < a)`
766    // via `__lt` with the operands swapped; 5.5 removed this and raises.
767    if event == TagMethod::Le
768        && matches!(
769            state.global().lua_version,
770            lua_types::LuaVersion::V51
771                | lua_types::LuaVersion::V52
772                | lua_types::LuaVersion::V53
773                | lua_types::LuaVersion::V54
774        )
775    {
776        let res_idx = state.top_idx();
777        // Mark the CallInfo: a `__lt` standing in for `__le`. If the `__lt`
778        // metamethod yields, `call_bin_tm` returns Err and the clear below is
779        // skipped, so the mark survives the yield and `vm::finish_op` negates
780        // the result on resume. C: `L->ci->callstatus |= CIST_LEQ`.
781        state.current_call_info_mut().callstatus |= crate::state::CIST_LEQ;
782        let called = call_bin_tm(state, p2, p1, res_idx, TagMethod::Lt)?;
783        // Synchronous return: clear the mark (C: `callstatus ^= CIST_LEQ`).
784        state.current_call_info_mut().callstatus &= !crate::state::CIST_LEQ;
785        if called {
786            // l_isfalse(result): a <= b  ==  not (b < a)
787            let result = state.get_at(res_idx).clone();
788            return Ok(matches!(result, LuaValue::Nil | LuaValue::Bool(false)));
789        }
790    }
791
792    Err(crate::debug::order_error(state, p1, p2))
793}
794
795// ── luaT_callorderiTM ────────────────────────────────────────────────────────
796
797//                            int flip, int isfloat, TMS event)
798/// Call an order metamethod where the second operand is a primitive int or float.
799///
800/// `v2` is a C `int`; `isfloat` selects whether it is coerced to
801/// `LuaValue::Float` (via `cast_num`) or kept as `LuaValue::Int`.
802/// When `flip` is true the operands are swapped so that `p1` was originally
803/// on the right-hand side.
804pub(crate) fn call_orderi_tm(
805    state: &mut LuaState,
806    p1: &LuaValue,
807    v2: i32,
808    flip: bool,
809    isfloat: bool,
810    event: TagMethod,
811) -> Result<bool, LuaError> {
812    //      setfltvalue(&aux, cast_num(v2));
813    //    }
814    //    else
815    //      setivalue(&aux, v2);
816    let aux = if isfloat {
817        LuaValue::Float(v2 as f64)
818    } else {
819        LuaValue::Int(v2 as i64)
820    };
821
822    //      p2 = p1; p1 = &aux;  /* correct them */
823    //    }
824    //    else
825    //      p2 = &aux;
826    if flip {
827        call_order_tm(state, &aux, p1, event)
828    } else {
829        call_order_tm(state, p1, &aux, event)
830    }
831}
832
833// ── luaT_adjustvarargs ───────────────────────────────────────────────────────
834
835//                              const Proto *p)
836/// Adjust the stack layout for a vararg function at call entry.
837///
838/// Moves the fixed parameters above the extra (vararg) arguments and copies
839/// the function object alongside them so the function body sees its registers
840/// at the expected offsets.  Records the extra-argument count in the CallInfo
841/// for `OP_VARARG` use.
842///
843/// Before call:  `[func | fixed... | extra...]`
844/// After call:   `[func | nil... | extra... | func′ | fixed...]`
845/// (`ci.func` and `ci.top` are advanced by `actual + 1`.)
846pub(crate) fn adjust_varargs(
847    state: &mut LuaState,
848    nfixparams: i32,
849    ci_idx: CallInfoIdx,
850    proto: &GcRef<lua_types::LuaProto>,
851) -> Result<(), LuaError> {
852    let ci_func: StackIdx = state.call_info[ci_idx.as_usize()].func;
853    let actual = (state.top_idx().0 as i32) - (ci_func.0 as i32) - 1;
854    let nextra = actual - nfixparams;
855    // TODO(phase-b): nextraargs lives inside CallInfoFrame::Lua; needs proper
856    // pattern-match write through state.call_info[..].u.
857    if let crate::state::CallInfoFrame::Lua {
858        ref mut nextraargs, ..
859    } = state.call_info[ci_idx.as_usize()].u
860    {
861        *nextraargs = nextra;
862    }
863
864    let maxstacksize = proto.maxstacksize as i32;
865    state.check_stack(maxstacksize + 1)?;
866
867    // Re-read ci_func after check_stack (stack may have reallocated, but
868    // StackIdx is index-based so the value is still correct).
869    let ci_func: StackIdx = state.call_info[ci_idx.as_usize()].func;
870
871    let func_val = state.get_at(ci_func).clone();
872    state.push(func_val);
873
874    //      setobjs2s(L, L->top.p++, ci->func.p + i);
875    //      setnilvalue(s2v(ci->func.p + i));  /* erase original parameter (for GC) */
876    //    }
877    for i in 1..=nfixparams {
878        // TODO(port): StackIdx is u32; if ci_func + i overflows the u32 range
879        // this panics in debug.  In practice `i` is small (≤ 255 params), but
880        // add a saturating or checked add in Phase B.
881        let src: StackIdx = ci_func + i as i32;
882        let param_val = state.get_at(src).clone();
883        state.push(param_val);
884        state.set_at(src, LuaValue::Nil);
885    }
886
887    // TODO(port): `actual + 1` may be negative if `actual < -1` (malformed call);
888    // casting to StackIdx (u32) would underflow.  In practice Lua guarantees
889    // actual >= 0 at this point, but add a debug_assert in Phase B.
890    let offset = (actual + 1) as i32;
891    state.call_info[ci_idx.as_usize()].func = state.call_info[ci_idx.as_usize()].func + offset;
892    state.call_info[ci_idx.as_usize()].top = state.call_info[ci_idx.as_usize()].top + offset;
893
894    if state.global().lua_version == lua_types::LuaVersion::V55 {
895        let base = state.call_info[ci_idx.as_usize()].func + 1;
896        state.set_at(base + nfixparams, LuaValue::Nil);
897    }
898
899    debug_assert!(state.top_idx().0 <= state.call_info[ci_idx.as_usize()].top.0);
900    Ok(())
901}
902
903// ── luaT_getvarargs ──────────────────────────────────────────────────────────
904
905/// Copy vararg values into the stack starting at `where_idx`.
906///
907/// `wanted` specifies how many values to copy.  Pass `wanted < 0` (the
908/// `LUA_MULTRET` convention) to request all available extra arguments; the
909/// stack top is then set to `where_idx + nextra`.
910///
911/// Slots beyond `nextra` but within `wanted` are filled with `LuaValue::Nil`.
912pub(crate) fn get_varargs(
913    state: &mut LuaState,
914    ci_idx: CallInfoIdx,
915    where_idx: StackIdx,
916    wanted: i32,
917) -> Result<(), LuaError> {
918    // Lua 5.5 named varargs (`function f(...t)`): `...` unpacks live from the
919    // shared table `t` (its `n` field is the count), so mutating `t` is
920    // observable through a later `...`. See `LuaProto.vararg_table_reg`.
921    let vatab_reg = state
922        .ci_lua_closure(ci_idx)
923        .and_then(|cl| cl.proto.vararg_table_reg);
924    if let Some(reg) = vatab_reg {
925        let base = state.ci_base(ci_idx);
926        if let LuaValue::Table(t) = state.get_at(base + reg as i32) {
927            let n_key = state.intern_str(b"n")?;
928            let nextra: i32 = match t.get(&LuaValue::Str(n_key)) {
929                LuaValue::Int(i) if i >= 0 && i <= (i32::MAX / 2) as i64 => i as i32,
930                _ => {
931                    return Err(LuaError::runtime(format_args!(
932                        "vararg table has no proper 'n'"
933                    )));
934                }
935            };
936            let wanted: i32 = if wanted < 0 {
937                state.check_stack(nextra)?;
938                state.gc().check_step();
939                state.set_top(where_idx + nextra);
940                nextra
941            } else {
942                wanted
943            };
944            let copy_count = wanted.min(nextra);
945            for i in 0..copy_count {
946                let val = t.get(&LuaValue::Int((i + 1) as i64));
947                state.set_at(where_idx + i as i32, val);
948            }
949            for i in copy_count..wanted {
950                state.set_at(where_idx + i as i32, LuaValue::Nil);
951            }
952            return Ok(());
953        }
954    }
955    let nextra: i32 = if let crate::state::CallInfoFrame::Lua { nextraargs, .. } =
956        state.call_info[ci_idx.as_usize()].u
957    {
958        nextraargs
959    } else {
960        0
961    };
962
963    //      wanted = nextra;  /* get all extra arguments available */
964    //      checkstackGCp(L, nextra, where);  /* ensure stack space */
965    //      L->top.p = where + nextra;  /* next instruction will need top */
966    //    }
967    let wanted: i32 = if wanted < 0 {
968        state.check_stack(nextra)?;
969        state.gc().check_step();
970        // TODO(port): `where_idx + nextra as i32` may overflow if nextra
971        // is very large; checked add in Phase B.
972        state.set_top(where_idx + nextra as i32);
973        nextra
974    } else {
975        wanted
976    };
977
978    //      setobjs2s(L, where + i, ci->func.p - nextra + i);
979    //
980    // After adjustvarargs, the extra args live at positions
981    // ci->func - nextra .. ci->func - 1.
982    let ci_func: StackIdx = state.call_info[ci_idx.as_usize()].func;
983    let copy_count = wanted.min(nextra);
984    for i in 0..copy_count {
985        // TODO(port): subtraction on StackIdx (u32) underflows if nextra > ci_func.
986        // Invariant: ci_func >= nextra (enforced by adjustvarargs), but add
987        // a debug_assert in Phase B.
988        let src: StackIdx = ci_func - nextra as i32 + i as i32;
989        let val = state.get_at(src).clone();
990        state.set_at(where_idx + i as i32, val);
991    }
992
993    //      setnilvalue(s2v(where + i));
994    for i in copy_count..wanted {
995        state.set_at(where_idx + i as i32, LuaValue::Nil);
996    }
997    Ok(())
998}
999
1000// ──────────────────────────────────────────────────────────────────────────────
1001// PORT STATUS
1002//   source:        src/ltm.c  (271 lines, 15 functions)
1003//                  src/ltm.h  (104 lines; merged into this file)
1004//   target_crate:  lua-vm
1005//   confidence:    medium
1006//   todos:         10
1007//   port_notes:    7
1008//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
1009//   notes:
1010//     Logic translation is faithful; the main uncertainties are:
1011//     (1) GcRef<LuaTable> accessor pattern (.metatable() / .borrow().metatable)
1012//         is TBD — annotated TODO(port) in get_tm_by_obj.
1013//     (2) call_tm / call_tm_res use a `do_call` / `do_call_no_yield` naming
1014//         convention that must be confirmed against the LuaState API in Phase B.
1015//     (3) call_order_tm uses `top_idx()` as a scratch slot matching C's
1016//         EXTRA_STACK convention — annotated with TODO(port) for Phase B review.
1017//     (4) StackIdx (u32) arithmetic in adjust_varargs / get_varargs can
1018//         underflow — annotated TODO(port); add checked arithmetic in Phase B.
1019//     (5) PERF(port) callout for obj_type_name Vec alloc retired: the
1020//         allocation-free `obj_type_name_cow` is now the canonical
1021//         implementation; `obj_type_name` is a compat wrapper.  Existing
1022//         callers can migrate to `obj_type_name_cow` to avoid the
1023//         `.into_owned()` allocation.
1024//     (6) LUA_COMPAT_LT_LE block in call_order_tm omitted per PORTING.md §13.
1025//     (7) intern_str fallibility in obj_type_name resolved: obj_type_name_cow
1026//         uses get_str_bytes (infallible) and obj_type_name wraps it with
1027//         Ok(...), so no OOM propagation risk remains.
1028//     (8) luaC_fix in init() is stubbed as gc().fix_object() — no-op Phase A-C.
1029// ──────────────────────────────────────────────────────────────────────────────