//! Tag methods (metamethods) — ported from `ltm.c` / `ltm.h`.
//!
//! Every metamethod name (`__index`, `__add`, …) is interned on `GlobalState`
//! during `init()`.  Lookup helpers (`get_tm`, `get_tm_by_obj`) return
//! `LuaValue::Nil` when no metamethod is present; callers check with
//! `value.is_nil()` (the `notm` macro in C).

use std::borrow::Cow;

#[allow(unused_imports)]
use crate::prelude::*;
use crate::state::LuaState;
use lua_types::{CallInfoIdx, GcRef, LuaError, LuaType, LuaValue, StackIdx};

// ── TagMethod enum (from ltm.h `TMS`) ────────────────────────────────────────

/// Metamethod selector; one variant per `__xxx` event, in ORDER TM.
///
/// The discriminant values are load-bearing: they index into
/// `GlobalState.tmname` and are used as bit positions in `Table.flags`.
/// Do **not** reorder without grepping ORDER TM / ORDER OP.
///
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub(crate) enum TagMethod {
    Index = 0,
    NewIndex,
    Gc,
    Mode,
    Len,
    Eq,
    Add,
    Sub,
    Mul,
    Mod,
    Pow,
    Div,
    IDiv,
    BAnd,
    BOr,
    BXor,
    Shl,
    Shr,
    Unm,
    BNot,
    Lt,
    Le,
    Concat,
    Call,
    Close,
    N,
}

impl TagMethod {
    /// Convert a raw u8 discriminant to a `TagMethod`.
    /// Returns `TagMethod::N` (sentinel) if `v >= TM_N`.
    ///
    /// PORT NOTE: reshaped for borrowck — C casts freely; Rust requires an explicit map.
    pub(crate) fn from_u8(v: u8) -> Self {
        match v {
            0 => TagMethod::Index,
            1 => TagMethod::NewIndex,
            2 => TagMethod::Gc,
            3 => TagMethod::Mode,
            4 => TagMethod::Len,
            5 => TagMethod::Eq,
            6 => TagMethod::Add,
            7 => TagMethod::Sub,
            8 => TagMethod::Mul,
            9 => TagMethod::Mod,
            10 => TagMethod::Pow,
            11 => TagMethod::Div,
            12 => TagMethod::IDiv,
            13 => TagMethod::BAnd,
            14 => TagMethod::BOr,
            15 => TagMethod::BXor,
            16 => TagMethod::Shl,
            17 => TagMethod::Shr,
            18 => TagMethod::Unm,
            19 => TagMethod::BNot,
            20 => TagMethod::Lt,
            21 => TagMethod::Le,
            22 => TagMethod::Concat,
            23 => TagMethod::Call,
            24 => TagMethod::Close,
            _ => TagMethod::N,
        }
    }
}

/// Number of real metamethods (= `TagMethod::N as usize`).
///
pub(crate) const TM_N: usize = TagMethod::N as usize;

// ── Type-name table (from ltm.h / ltm.c `luaT_typenames_`) ──────────────────

//
//   "no value",
//   "nil", "boolean", udatatypename, "number",
//   "string", "table", "function", udatatypename, "thread",
//   "upvalue", "proto"
// };
//
// Indexed as `luaT_typenames_[(x) + 1]` where x is the raw LuaType integer
// (LUA_TNONE = -1, LUA_TNIL = 0, …, LUA_TPROTO = 10).
// LUA_TOTALTYPES = LUA_TPROTO + 2 = 12 entries.
//
// PORT NOTE: C uses `const char*`; Rust uses `&'static [u8]` throughout.
pub(crate) static TYPE_NAMES: &[&[u8]] = &[
    b"no value", // index 0 → LUA_TNONE  (-1 + 1)
    b"nil",      // index 1 → LUA_TNIL   ( 0 + 1)
    b"boolean",  // index 2 → LUA_TBOOLEAN
    b"userdata", // index 3 → LUA_TLIGHTUSERDATA
    b"number",   // index 4 → LUA_TNUMBER
    b"string",   // index 5 → LUA_TSTRING
    b"table",    // index 6 → LUA_TTABLE
    b"function", // index 7 → LUA_TFUNCTION
    b"userdata", // index 8 → LUA_TUSERDATA
    b"thread",   // index 9 → LUA_TTHREAD
    b"upvalue",  // index 10 → LUA_TUPVAL
    b"proto",    // index 11 → LUA_TPROTO
];

/// Return the human-readable type name for a `LuaType`.
///
///
/// Panics in debug builds if `t` is out of the expected range (shouldn't
/// happen with a well-formed `LuaType`).
pub(crate) fn type_name(t: LuaType) -> &'static [u8] {
    let idx = (t as i32 + 1) as usize;
    TYPE_NAMES.get(idx).copied().unwrap_or(b"?")
}

// ── luaT_init ────────────────────────────────────────────────────────────────

/// Intern all metamethod name strings and pin them in the GC.
///
/// Must be called exactly once during `LuaState` initialization, before any
/// metamethod lookup.  After this call, `GlobalState.tmname[i]` holds the
/// interned `LuaString` for metamethod `i`.
pub(crate) fn init(state: &mut LuaState) -> Result<(), LuaError> {
    //     "__index", "__newindex",
    //     "__gc", "__mode", "__len", "__eq",
    //     "__add", "__sub", "__mul", "__mod", "__pow",
    //     "__div", "__idiv",
    //     "__band", "__bor", "__bxor", "__shl", "__shr",
    //     "__unm", "__bnot", "__lt", "__le",
    //     "__concat", "__call", "__close"
    // };
    const EVENT_NAMES: &[&[u8]] = &[
        b"__index",
        b"__newindex",
        b"__gc",
        b"__mode",
        b"__len",
        b"__eq",
        b"__add",
        b"__sub",
        b"__mul",
        b"__mod",
        b"__pow",
        b"__div",
        b"__idiv",
        b"__band",
        b"__bor",
        b"__bxor",
        b"__shl",
        b"__shr",
        b"__unm",
        b"__bnot",
        b"__lt",
        b"__le",
        b"__concat",
        b"__call",
        b"__close",
    ];
    debug_assert!(EVENT_NAMES.len() == TM_N);

    // PORT NOTE: The C `tmname[TM_N]` is a fixed-size array on `global_State`;
    // the Rust port uses `Vec<GcRef<LuaString>>` initialized empty in
    // `lua_state_init`, so we must grow it to `TM_N` before indexed assignment.
    if state.global().tmname.len() < TM_N {
        let pad = state.intern_str(b"")?;
        state.global_mut().tmname.resize(TM_N, pad);
    }

    //     G(L)->tmname[i] = luaS_new(L, luaT_eventname[i]);
    //     luaC_fix(L, obj2gco(G(L)->tmname[i]));  /* never collect these names */
    // }
    for (i, &name) in EVENT_NAMES.iter().enumerate() {
        let interned = state.intern_str(name)?;
        state.global_mut().tmname[i] = interned.clone();
        // Pin the string so the GC never collects it.
        // TODO(port): luaC_fix API on gc() is TBD; no-op in Phase A–C (Rc keeps it alive)
        state.gc().fix_object(&interned);
    }
    Ok(())
}

// ── luaT_gettmbyobj ──────────────────────────────────────────────────────────

/// Look up a metamethod for any Lua value by dispatching on its type.
///
/// Tables and full userdata have per-object metatables; all other types use
/// the per-type metatables on `GlobalState`.  Returns `LuaValue::Nil` when
/// neither the object nor its type has a metatable, or when the metatable
/// does not contain the requested metamethod.
pub(crate) fn get_tm_by_obj(state: &mut LuaState, o: &LuaValue, event: TagMethod) -> LuaValue {
    //   case LUA_TTABLE:    mt = hvalue(o)->metatable; break;
    //   case LUA_TUSERDATA: mt = uvalue(o)->metatable; break;
    //   default:            mt = G(L)->mt[ttype(o)];
    // }
    //
    // TODO(port): `GcRef<LuaTable>` access pattern (direct field vs borrow) is
    // TBD pending the GcRef/RefCell decision in Phase B; using `.metatable()`
    // accessor here as a placeholder.
    let mt: Option<GcRef<lua_types::value::LuaTable>> = match o {
        LuaValue::Table(t) => t.metatable(),
        LuaValue::UserData(u) => u.metatable(),
        _ => {
            let type_idx = o.base_type() as usize;
            state.global().mt[type_idx].clone()
        }
    };

    match mt {
        Some(mt_ref) => {
            // Clone the name string before the table lookup to avoid borrow conflict.
            let ename = state.global().tmname[event as usize].clone();
            mt_ref.get_short_str(&ename)
        }
        None => LuaValue::Nil,
    }
}

// ── luaT_objtypename ─────────────────────────────────────────────────────────

/// Return the human-readable type name for a Lua value without any heap
/// allocation in the common case.
///
/// For tables and full userdata whose metatable defines `__name` as a string,
/// returns `Cow::Owned` with the custom name bytes.  For every other case
/// returns `Cow::Borrowed` pointing into the static `TYPE_NAMES` table —
/// no allocation, no interning, no `LuaState` access required.
///
/// PORT NOTE: C returns `const char*` — either a pointer into a GC-managed
/// `LuaString` or a pointer into the static `luaT_typenames_` array.  Rust
/// models this as `Cow<'static, [u8]>`: `Borrowed` for static names,
/// `Owned` only when the metatable `__name` field overrides the default.
/// Uses `LuaTable::get_str_bytes` (linear byte-scan) instead of
/// `intern_str` + `get_short_str` so the lookup is infallible and requires
/// no mutable state access.
pub(crate) fn obj_type_name_cow(o: &LuaValue, honors_name: bool) -> Cow<'static, [u8]> {
    if matches!(o, LuaValue::LightUserData(_)) {
        return Cow::Borrowed(b"light userdata");
    }
    //        (ttisfulluserdata(o) && (mt = uvalue(o)->metatable) != NULL))
    let mt: Option<GcRef<lua_types::value::LuaTable>> = match o {
        LuaValue::Table(t) => t.metatable(),
        LuaValue::UserData(u) => u.metatable(),
        _ => None,
    };
    if honors_name {
        if let Some(mt_ref) = mt {
            // Uses get_str_bytes (raw byte scan) rather than intern_str + get_short_str
            // so no mutable state is needed and no error can propagate.
            let name_val = mt_ref.get_str_bytes(b"__name");
            if let LuaValue::Str(s) = name_val {
                return Cow::Owned(s.as_bytes().to_vec());
            }
        }
    }
    Cow::Borrowed(type_name(o.base_type()))
}

/// Compatibility wrapper returning `Vec<u8>` for callers that have not yet
/// migrated to `obj_type_name_cow`.  Always allocates; prefer
/// `obj_type_name_cow` for allocation-free lookup in error-path code.
///
/// `state` supplies the active version: the `__name` metafield override is a
/// 5.3 addition, so 5.1/5.2 always report the primitive type name (VM finding
/// F2). Fallibility (`Result`) is retained for API compatibility.
pub(crate) fn obj_type_name(state: &mut LuaState, o: &LuaValue) -> Result<Vec<u8>, LuaError> {
    let honors_name = state.global().lua_version.honors_name_metafield();
    Ok(obj_type_name_cow(o, honors_name).into_owned())
}

// ── luaT_callTM ──────────────────────────────────────────────────────────────

//                      const TValue *p2, const TValue *p3)
/// Call tag method `f` with three arguments, discarding any return values.
///
/// Used for metamethods like `__gc` and `__close` that take three operands
/// and whose return value is irrelevant.
///
/// If the current call frame is Lua bytecode (`isLuacode`), the metamethod
/// may yield; otherwise yielding is suppressed (`callnoyield`).
pub(crate) fn call_tm(
    state: &mut LuaState,
    f: LuaValue,
    p1: LuaValue,
    p2: LuaValue,
    p3: LuaValue,
) -> Result<(), LuaError> {
    let func = state.top_idx();
    //
    // PORT NOTE: In C these are direct writes into the EXTRA_STACK reserve
    // area above the official top.  In Rust we use push() which manages
    // capacity; the semantic result is identical.
    state.push(f);
    state.push(p1);
    state.push(p2);
    state.push(p3);
    //      luaD_call(L, func, 0);
    //    else
    //      luaD_callnoyield(L, func, 0);
    //
    // TODO(port): `do_call(func, nresults)` vs `call_from(func, nresults)` —
    // exact Rust API name TBD; nargs is implicit as `top - func - 1`.
    if state.current_ci().is_lua_code() {
        state.do_call(func, 0)?;
    } else {
        state.do_call_no_yield(func, 0)?;
    }
    Ok(())
}

// ── luaT_callTMres ───────────────────────────────────────────────────────────

//                          const TValue *p2, StkId res)
/// Call tag method `f` with two arguments, writing the single result into
/// the stack slot at index `res`.
///
/// `res` is a `StackIdx` (index-stable across stack reallocation) that must
/// refer to a pre-existing or scratch slot.  After return, the stack top is
/// back to what it was before the call (i.e. `top == res`).
///
/// C uses `savestack`/`restorestack` (byte-offset from base) to preserve `res`
/// across potential stack reallocations inside `luaD_call`.  In Rust, `StackIdx`
/// is already an index and needs no save/restore.
pub(crate) fn call_tm_res(
    state: &mut LuaState,
    f: LuaValue,
    p1: LuaValue,
    p2: LuaValue,
    res: StackIdx,
) -> Result<(), LuaError> {
    // savestack → StackIdx is already the stable byte-offset analogue; no-op.

    let func = state.top_idx();
    state.push(f);
    state.push(p1);
    state.push(p2);

    //      luaD_call(L, func, 1);
    //    else
    //      luaD_callnoyield(L, func, 1);
    //
    // TODO(port): same `do_call` API question as in call_tm above.
    if state.current_ci().is_lua_code() {
        state.do_call(func, 1)?;
    } else {
        state.do_call_no_yield(func, 1)?;
    }

    // restorestack → StackIdx is already stable; `res` is unchanged.

    // Pre-decrement top, then copy that slot to res.
    let result_val = state.pop();
    state.set_at(res, result_val);
    Ok(())
}

// ── callbinTM (static) ────────────────────────────────────────────────────────

//                           StkId res, TMS event)
/// Try to find and call a binary tag method for `event`.
///
/// Checks `p1` first, then `p2`, for a metamethod.  If neither has one,
/// returns `false` and leaves `res` unmodified.  If found, calls it with
/// `(p1, p2)` as arguments, writes the result to slot `res`, and returns `true`.
fn call_bin_tm(
    state: &mut LuaState,
    p1: &LuaValue,
    p2: &LuaValue,
    res: StackIdx,
    event: TagMethod,
) -> Result<bool, LuaError> {
    let tm = get_tm_by_obj(state, p1, event);
    //      tm = luaT_gettmbyobj(L, p2, event);  /* try second operand */
    let tm = if tm.is_nil() {
        get_tm_by_obj(state, p2, event)
    } else {
        tm
    };
    if tm.is_nil() {
        return Ok(false);
    }
    // Clone p1/p2 before the mutable borrow of `state` via call_tm_res.
    call_tm_res(state, tm, p1.clone(), p2.clone(), res)?;
    Ok(true)
}

/// Lua 5.3 core string→integer coercion for bitwise ops.
///
/// Returns:
/// - `Some(Ok(()))` — both operands coerced to integers; the op was computed
///   and written to `res`.
/// - `Some(Err(..))` — an operand is a numeric-but-non-integral string
///   (e.g. `"3.5"`, `"0xff..ff.0"`); raises "number has no integer
///   representation" (`luaG_tointerror`), matching lua5.3.6.
/// - `None` — coercion is impossible (an operand is a non-numeric string such
///   as `"abc"`); the caller falls through to its normal error path, which
///   raises "perform bitwise operation on".
///
/// The 5.3-only gate and the bitwise-event filter are applied by the caller.
fn try_bitwise_strconv_53(
    state: &mut LuaState,
    p1: &LuaValue,
    p1_idx: Option<StackIdx>,
    p2: &LuaValue,
    p2_idx: Option<StackIdx>,
    res: StackIdx,
    event: TagMethod,
) -> Option<Result<(), LuaError>> {
    // Both operands must be number-ish (integer, float, or a string that
    // parses as a number). If either is genuinely non-numeric, bail to the
    // caller's "perform bitwise operation on" path.
    let n1 = p1.to_number_with_strconv();
    let n2 = p2.to_number_with_strconv();
    if n1.is_none() || n2.is_none() {
        return None;
    }
    // Both are number-ish. Now require integer representations. If a number-ish
    // operand has no integer representation (non-integral numeric string or
    // float), 5.3 raises "number has no integer representation".
    let i1 = p1.to_integer_with_strconv();
    let i2 = p2.to_integer_with_strconv();
    let (i1, i2) = match (i1, i2) {
        (Some(a), Some(b)) => (a, b),
        _ => {
            let p1_idx = p1_idx.unwrap_or(StackIdx(0));
            let p2_idx = p2_idx.unwrap_or(StackIdx(0));
            return Some(Err(crate::debug::to_int_error(
                state,
                p1,
                Some(p1_idx),
                p2,
                Some(p2_idx),
            )));
        }
    };
    let result = match event {
        TagMethod::BAnd => i1 & i2,
        TagMethod::BOr => i1 | i2,
        TagMethod::BXor => i1 ^ i2,
        TagMethod::Shl => crate::vm::shiftl(i1, i2),
        TagMethod::Shr => crate::vm::shiftl(i1, i2.wrapping_neg()),
        // Unary `~x` arrives here as a binary event with p1 == p2; `~i1`.
        TagMethod::BNot => !i1,
        _ => return None,
    };
    state.set_at(res, LuaValue::Int(result));
    Some(Ok(()))
}

// ── luaT_trybinTM ────────────────────────────────────────────────────────────

//                         StkId res, TMS event)
/// Attempt a binary metamethod call; raise a type error if no metamethod exists.
///
/// For bitwise operations, further distinguishes between:
/// - Both operands are numbers but not integers → `LuaError::int_overflow`
///   (`luaG_tointerror` — "number has no integer representation")
/// - At least one operand is not a number → `LuaError::arith_error` with
///   "perform bitwise operation on"
///
/// All other missing metamethods raise `LuaError::arith_error` with
/// "perform arithmetic on".
pub(crate) fn try_bin_tm(
    state: &mut LuaState,
    p1: &LuaValue,
    p1_idx: Option<StackIdx>,
    p2: &LuaValue,
    p2_idx: Option<StackIdx>,
    res: StackIdx,
    event: TagMethod,
) -> Result<(), LuaError> {
    // Lua 5.3 arith error wording with string operands. In the shared (5.4)
    // model arithmetic metamethods (`__add`/`__sub`/…) are installed on the
    // string metatable, so a failed arith fast path that has a string operand
    // dispatches to the string-library `trymt`, which raises `attempt to <op> a
    // '<t>' with a '<t>'`, adds a spurious `[C]: in metamethod '<op>'` frame, and
    // cannot produce operand varinfo (it runs as a C function with no access to
    // the calling bytecode registers). 5.3 instead owns arithmetic string
    // coercion in the core: when the operation cannot succeed it raises `attempt
    // to perform arithmetic on a <type> value (<varinfo>)`, blaming the operand
    // that does not coerce to a number.
    //
    // The intercept is narrow: it fires ONLY when a string operand cannot be
    // coerced to a number AND the other operand carries no genuine arith
    // metamethod of its own. So `t + "5"` (t has `__add`) still dispatches to
    // t's metamethod via `call_bin_tm`, and the coercible success path
    // (`"3" + 2`) still flows through the string metamethod below, preserving
    // 5.3 float-promotion semantics. See specs/followup/5.3-coerce-err.md.
    //
    // 5.1/5.2 own arithmetic string coercion in the core the same way: a
    // non-coercible string operand raises `attempt to perform arithmetic on a
    // <type> value` (`luaG_aritherror` → `luaG_typeerror`, no metamethod-name
    // attribution and no spurious `[C]: in metamethod` frame). They share this
    // intercept; the per-version message ordering is applied downstream in
    // `debug::type_error`.
    if matches!(
        state.global().lua_version,
        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
    )
        && matches!(
            event,
            TagMethod::Add
                | TagMethod::Sub
                | TagMethod::Mul
                | TagMethod::Mod
                | TagMethod::Pow
                | TagMethod::Div
                | TagMethod::IDiv
                | TagMethod::Unm
        )
        && (matches!(p1, LuaValue::Str(_)) || matches!(p2, LuaValue::Str(_)))
    {
        use crate::state::LuaValueExt;
        let p1_num = p1.to_number_with_strconv().is_some();
        let p2_num = p2.to_number_with_strconv().is_some();
        if !(p1_num && p2_num) {
            // A string operand did not coerce. Only raise the core error here if
            // the non-string operand has no genuine arith metamethod; otherwise
            // fall through so `call_bin_tm` dispatches to that real metamethod
            // (the string metatable's synthetic arith mm is ignored for this
            // decision — it never produces a useful result for a non-coercible
            // pairing, it only raises the wrong-version wording).
            //
            // Unary minus arrives as a binary event with `p1 == p2`, so there is
            // no genuine "other operand" — the only metamethod available is the
            // synthetic string one. Treat it as absent so `-"x"` takes the core
            // path.
            let unary = matches!(event, TagMethod::Unm);
            let other_has_mm = !unary
                && if matches!(p1, LuaValue::Str(_)) {
                    !get_tm_by_obj(state, p2, event).is_nil()
                } else {
                    !get_tm_by_obj(state, p1, event).is_nil()
                };
            if !other_has_mm {
                // Point varinfo at the operand that does not coerce, matching C
                // `luaG_opinterror`. A coercible numeric string counts as a
                // number, so `'2' * nil` blames `nil`, not `'2'`.
                let (bad, bad_idx) = if !p1_num {
                    (p1, p1_idx.unwrap_or(StackIdx(0)))
                } else {
                    (p2, p2_idx.unwrap_or(StackIdx(0)))
                };
                return Err(crate::debug::arith_type_error(
                    state,
                    bad,
                    bad_idx,
                    b"perform arithmetic on",
                    !unary,
                ));
            }
        }
    }
    if !call_bin_tm(state, p1, p2, res, event)? {
        // Lua 5.3 coerces numeric strings to integers in the *core* bitwise
        // ops (`& | ~ << >>` and unary `~`), where 5.4/5.5 require a real
        // number operand and delegate string handling to a (non-existent)
        // string metamethod. On the 5.3 path, after the metamethod lookup
        // fails, retry the operation with string→integer coercion before
        // raising. The boundary semantics (non-integral numeric string →
        // "no integer representation"; non-numeric string → "perform bitwise
        // operation") fall out of `to_integer_with_strconv` /
        // `to_number_with_strconv`. See specs/followup/5.3-coerce-err.md.
        if matches!(state.global().lua_version, lua_types::LuaVersion::V53)
            && matches!(
                event,
                TagMethod::BAnd
                    | TagMethod::BOr
                    | TagMethod::BXor
                    | TagMethod::Shl
                    | TagMethod::Shr
                    | TagMethod::BNot
            )
            && (matches!(p1, LuaValue::Str(_)) || matches!(p2, LuaValue::Str(_)))
        {
            if let Some(result) = try_bitwise_strconv_53(state, p1, p1_idx, p2, p2_idx, res, event)
            {
                return result;
            }
        }
        //   case TM_BAND: case TM_BOR: case TM_BXOR:
        //   case TM_SHL: case TM_SHR: case TM_BNOT: {
        //     if (ttisnumber(p1) && ttisnumber(p2))
        //       luaG_tointerror(L, p1, p2);
        //     else
        //       luaG_opinterror(L, p1, p2, "perform bitwise operation on");
        //   }
        //   /* calls never return, but to avoid warnings: *//* FALLTHROUGH */
        //   default:
        //     luaG_opinterror(L, p1, p2, "perform arithmetic on");
        // }
        //
        // PORT NOTE: the C switch has a dead "FALLTHROUGH" for bitwise cases
        // because both branches of the inner if/else call noreturn functions.
        // In Rust `match` has no implicit fallthrough; each arm is self-contained
        // and explicitly returns `Err(...)`.
        match event {
            TagMethod::BAnd
            | TagMethod::BOr
            | TagMethod::BXor
            | TagMethod::Shl
            | TagMethod::Shr
            | TagMethod::BNot => {
                if matches!(p1, LuaValue::Int(_) | LuaValue::Float(_))
                    && matches!(p2, LuaValue::Int(_) | LuaValue::Float(_))
                {
                    // "(field 'huge')" / "(local 'x')" etc. based on the bytecode that
                    // produced the bad operand.
                    return Err(crate::debug::to_int_error(state, p1, p1_idx, p2, p2_idx));
                } else {
                    // varinfo on the non-number operand.
                    let p1_idx = p1_idx.unwrap_or(StackIdx(0));
                    let p2_idx = p2_idx.unwrap_or(StackIdx(0));
                    return Err(crate::debug::op_int_error(
                        state,
                        p1,
                        p1_idx,
                        p2,
                        p2_idx,
                        b"perform bitwise operation on",
                    ));
                }
            }
            _ => {
                // varinfo enriches with "(global 'aaa')" etc.
                let p1_idx = p1_idx.unwrap_or(StackIdx(0));
                let p2_idx = p2_idx.unwrap_or(StackIdx(0));
                return Err(crate::debug::op_int_error(
                    state,
                    p1,
                    p1_idx,
                    p2,
                    p2_idx,
                    b"perform arithmetic on",
                ));
            }
        }
    }
    Ok(())
}

// ── luaT_tryconcatTM ─────────────────────────────────────────────────────────

/// Attempt the `__concat` metamethod on the two values at the top of the stack.
///
/// Reads `stack[top-2]` and `stack[top-1]`, searches both for `__concat`,
/// calls it with `(stack[top-2], stack[top-1])` writing the result back to
/// `stack[top-2]`, or raises `LuaError::concat_error` if no metamethod exists.
pub(crate) fn try_concat_tm(state: &mut LuaState) -> Result<(), LuaError> {
    let top = state.top_idx();
    //      luaG_concaterror(L, s2v(top - 2), s2v(top - 1));
    //
    // Clone the operands before any call that might mutate the stack.
    let p1 = state.get_at(top - 2).clone();
    let p2 = state.get_at(top - 1).clone();
    if !call_bin_tm(state, &p1, &p2, top - 2, TagMethod::Concat)? {
        let p1_ok = matches!(p1, LuaValue::Str(_) | LuaValue::Int(_) | LuaValue::Float(_));
        let (bad, bad_idx) = if p1_ok {
            (&p2, top - 1)
        } else {
            (&p1, top - 2)
        };
        return Err(crate::debug::type_error(
            state,
            bad,
            bad_idx,
            b"concatenate",
        ));
    }
    Ok(())
}

// ── luaT_trybinassocTM ───────────────────────────────────────────────────────

//                              int flip, StkId res, TMS event)
/// Try a binary associative metamethod, optionally swapping the operands.
///
/// When `flip` is `true`, operands are exchanged before dispatch.  This
/// implements Lua's symmetry rule: if `a OP b` finds no metamethod on `a`,
/// the VM retries with `b OP a` (setting flip=true to restore the original
/// argument order for the call).
pub(crate) fn try_bin_assoc_tm(
    state: &mut LuaState,
    p1: &LuaValue,
    p1_idx: Option<StackIdx>,
    p2: &LuaValue,
    p2_idx: Option<StackIdx>,
    flip: bool,
    res: StackIdx,
    event: TagMethod,
) -> Result<(), LuaError> {
    //      luaT_trybinTM(L, p2, p1, res, event);
    //    else
    //      luaT_trybinTM(L, p1, p2, res, event);
    if flip {
        try_bin_tm(state, p2, p2_idx, p1, p1_idx, res, event)
    } else {
        try_bin_tm(state, p1, p1_idx, p2, p2_idx, res, event)
    }
}

// ── luaT_trybiniTM ───────────────────────────────────────────────────────────

//                          int flip, StkId res, TMS event)
/// Try a binary metamethod where the second operand is an integer constant.
///
/// Boxes `i2` as `LuaValue::Int` and delegates to `try_bin_assoc_tm`.
pub(crate) fn try_bini_tm(
    state: &mut LuaState,
    p1: &LuaValue,
    p1_idx: Option<StackIdx>,
    i2: i64,
    flip: bool,
    res: StackIdx,
    event: TagMethod,
) -> Result<(), LuaError> {
    let aux = LuaValue::Int(i2);
    // The immediate operand has no stack location, so it gets `None`.
    try_bin_assoc_tm(state, p1, p1_idx, &aux, None, flip, res, event)
}

// ── get_compTM / get_equalTM (Lua 5.1 same-reference rule) ───────────────────

/// Resolve the metatable governing an operand, mirroring `get_tm_by_obj`'s
/// metatable dispatch: tables and full userdata carry per-object metatables;
/// every other type uses the per-type metatable on `GlobalState`.
fn operand_metatable(
    state: &LuaState,
    o: &LuaValue,
) -> Option<GcRef<lua_types::value::LuaTable>> {
    match o {
        LuaValue::Table(t) => t.metatable(),
        LuaValue::UserData(u) => u.metatable(),
        _ => {
            let type_idx = o.base_type() as usize;
            state.global().mt[type_idx].clone()
        }
    }
}

/// Lua 5.1's `get_compTM`/`get_equalTM`: the comparison/equality metamethod is
/// honoured only when BOTH operands resolve to the SAME handler function.
///
/// Returns the chosen metamethod, or `LuaValue::Nil` when the handlers differ,
/// when one operand lacks the metamethod, or when neither has a metatable.
/// 5.1 raised an error for ordered comparisons in this `Nil` case (the caller
/// does so) and returned "not equal" for equality.
///
/// PORT NOTE (#events51): 5.1 picks the left metatable's handler, returns it
/// directly when both metatables are the same object, and otherwise keeps it
/// only if the right metatable's handler is raw-equal (same function reference).
/// 5.2+ consult left-then-right unconditionally, so this is gated to V51 by the
/// caller.
pub(crate) fn get_comp_tm_51(
    state: &mut LuaState,
    p1: &LuaValue,
    p2: &LuaValue,
    event: TagMethod,
) -> LuaValue {
    let tm1 = get_tm_by_obj(state, p1, event);
    if tm1.is_nil() {
        return LuaValue::Nil;
    }
    let mt1 = operand_metatable(state, p1);
    let mt2 = operand_metatable(state, p2);
    if let (Some(a), Some(b)) = (&mt1, &mt2) {
        if GcRef::ptr_eq(a, b) {
            return tm1;
        }
    }
    let tm2 = get_tm_by_obj(state, p2, event);
    if tm2.is_nil() {
        return LuaValue::Nil;
    }
    if crate::vm::raw_equal_values(&tm1, &tm2) {
        tm1
    } else {
        LuaValue::Nil
    }
}

// ── luaT_callorderTM ─────────────────────────────────────────────────────────

//                           TMS event)
/// Call an order metamethod (`__lt` or `__le`) and return its boolean result.
///
/// Returns `true` if the metamethod returned a truthy value.
/// Raises `LuaError::order_error` if neither operand has the metamethod.
///
/// PORT NOTE: `LUA_COMPAT_LT_LE` (deriving `__le` from `__lt`) is ON by default
/// in the reference `make macosx` builds of 5.1–5.4 and removed in 5.5. We match
/// the default-built reference (the pinned oracle, per specs/oracle/CONTRACT.md),
/// so the fallback is implemented and version-gated: derive for 5.1–5.4, raise
/// for 5.5.
pub(crate) fn call_order_tm(
    state: &mut LuaState,
    p1: &LuaValue,
    p2: &LuaValue,
    event: TagMethod,
) -> Result<bool, LuaError> {
    // PORT NOTE (#139): In 5.1, `luaV_lessthan`/`luaV_lessequal` check
    // `ttype(l) != ttype(r)` FIRST and raise `luaG_ordererror` before any TM
    // lookup, so the `__lt`/`__le` metamethod is consulted ONLY for same-Lua-type
    // operands. 5.2+ removed that guard and consults the TM for mixed types too.
    // Both-number and both-string operands are resolved by fast paths and never
    // reach this choke point, so a single gate here on the Lua type tag (Int and
    // Float share the `Number` tag via `base_type`, matching C's `ttype`)
    // reproduces 5.1's check order. This is a cold path: a direct `lua_version`
    // read is correct, mirroring the `__le`-from-`__lt` derivation gate below.
    {
        use crate::state::LuaValueExt;
        if state.global().lua_version == lua_types::LuaVersion::V51
            && p1.base_type() != p2.base_type()
        {
            return Err(crate::debug::order_error(state, p1, p2));
        }
    }

    // PORT NOTE (#events51): 5.1's `call_orderTM` requires both operands to
    // carry the SAME `__lt`/`__le` handler (`luaO_rawequalObj(tm1, tm2)`), and
    // raises `luaG_ordererror` otherwise — including when only one operand has
    // the metamethod. 5.2+ consult left-then-right unconditionally, so this is
    // gated to V51. The `__le`→`__lt` (swapped) derivation below uses the same
    // same-reference rule.
    if state.global().lua_version == lua_types::LuaVersion::V51 {
        let res_idx = state.top_idx();
        let tm = get_comp_tm_51(state, p1, p2, event);
        if !tm.is_nil() {
            call_tm_res(state, tm, p1.clone(), p2.clone(), res_idx)?;
            let result = state.get_at(res_idx).clone();
            return Ok(!matches!(result, LuaValue::Nil | LuaValue::Bool(false)));
        }
        if event == TagMethod::Le {
            let tm = get_comp_tm_51(state, p2, p1, TagMethod::Lt);
            if !tm.is_nil() {
                state.current_call_info_mut().callstatus |= crate::state::CIST_LEQ;
                call_tm_res(state, tm, p2.clone(), p1.clone(), res_idx)?;
                state.current_call_info_mut().callstatus &= !crate::state::CIST_LEQ;
                let result = state.get_at(res_idx).clone();
                return Ok(matches!(result, LuaValue::Nil | LuaValue::Bool(false)));
            }
        }
        return Err(crate::debug::order_error(state, p1, p2));
    }

    //      return !l_isfalse(s2v(L->top.p));
    //
    // PORT NOTE: In C, `L->top.p` is used as a scratch slot (written by
    // callTMres then immediately read) in the EXTRA_STACK reserved area above
    // the official stack top — the stack top is NOT officially advanced.
    // In Rust we pass `state.top_idx()` as `res`; call_bin_tm → call_tm_res
    // pushes 3 values, calls, pops the result back to res, and leaves top ==
    // res (i.e. top unchanged relative to entry).  Reading get_at(res_idx)
    // after this is safe because the slot was just written and top == res_idx.
    //
    // TODO(port): Verify in Phase B that no call path between call_bin_tm
    // returning and the get_at read can disturb the scratch slot or reset top
    // below res_idx.  The invariant holds as long as do_call with 1 result
    // leaves exactly one value on the stack above func.
    let res_idx = state.top_idx();
    if call_bin_tm(state, p1, p2, res_idx, event)? {
        // l_isfalse(o) → matches!(o, LuaValue::Nil | LuaValue::Bool(false))
        let result = state.get_at(res_idx).clone();
        return Ok(!matches!(result, LuaValue::Nil | LuaValue::Bool(false)));
    }

    // LUA_COMPAT_LT_LE: in 5.1–5.4 a missing `__le` falls back to `not (b < a)`
    // via `__lt` with the operands swapped; 5.5 removed this and raises.
    if event == TagMethod::Le
        && matches!(
            state.global().lua_version,
            lua_types::LuaVersion::V51
                | lua_types::LuaVersion::V52
                | lua_types::LuaVersion::V53
                | lua_types::LuaVersion::V54
        )
    {
        let res_idx = state.top_idx();
        // Mark the CallInfo: a `__lt` standing in for `__le`. If the `__lt`
        // metamethod yields, `call_bin_tm` returns Err and the clear below is
        // skipped, so the mark survives the yield and `vm::finish_op` negates
        // the result on resume. C: `L->ci->callstatus |= CIST_LEQ`.
        state.current_call_info_mut().callstatus |= crate::state::CIST_LEQ;
        let called = call_bin_tm(state, p2, p1, res_idx, TagMethod::Lt)?;
        // Synchronous return: clear the mark (C: `callstatus ^= CIST_LEQ`).
        state.current_call_info_mut().callstatus &= !crate::state::CIST_LEQ;
        if called {
            // l_isfalse(result): a <= b  ==  not (b < a)
            let result = state.get_at(res_idx).clone();
            return Ok(matches!(result, LuaValue::Nil | LuaValue::Bool(false)));
        }
    }

    Err(crate::debug::order_error(state, p1, p2))
}

// ── luaT_callorderiTM ────────────────────────────────────────────────────────

//                            int flip, int isfloat, TMS event)
/// Call an order metamethod where the second operand is a primitive int or float.
///
/// `v2` is a C `int`; `isfloat` selects whether it is coerced to
/// `LuaValue::Float` (via `cast_num`) or kept as `LuaValue::Int`.
/// When `flip` is true the operands are swapped so that `p1` was originally
/// on the right-hand side.
pub(crate) fn call_orderi_tm(
    state: &mut LuaState,
    p1: &LuaValue,
    v2: i32,
    flip: bool,
    isfloat: bool,
    event: TagMethod,
) -> Result<bool, LuaError> {
    //      setfltvalue(&aux, cast_num(v2));
    //    }
    //    else
    //      setivalue(&aux, v2);
    let aux = if isfloat {
        LuaValue::Float(v2 as f64)
    } else {
        LuaValue::Int(v2 as i64)
    };

    //      p2 = p1; p1 = &aux;  /* correct them */
    //    }
    //    else
    //      p2 = &aux;
    if flip {
        call_order_tm(state, &aux, p1, event)
    } else {
        call_order_tm(state, p1, &aux, event)
    }
}

// ── luaT_adjustvarargs ───────────────────────────────────────────────────────

//                              const Proto *p)
/// Adjust the stack layout for a vararg function at call entry.
///
/// Moves the fixed parameters above the extra (vararg) arguments and copies
/// the function object alongside them so the function body sees its registers
/// at the expected offsets.  Records the extra-argument count in the CallInfo
/// for `OP_VARARG` use.
///
/// Before call:  `[func | fixed... | extra...]`
/// After call:   `[func | nil... | extra... | func′ | fixed...]`
/// (`ci.func` and `ci.top` are advanced by `actual + 1`.)
pub(crate) fn adjust_varargs(
    state: &mut LuaState,
    nfixparams: i32,
    ci_idx: CallInfoIdx,
    proto: &GcRef<lua_types::LuaProto>,
) -> Result<(), LuaError> {
    let ci_func: StackIdx = state.call_info[ci_idx.as_usize()].func;
    let actual = (state.top_idx().0 as i32) - (ci_func.0 as i32) - 1;
    let nextra = actual - nfixparams;
    state.call_info[ci_idx.as_usize()].set_nextra_args(nextra);

    let maxstacksize = proto.maxstacksize as i32;
    state.check_stack(maxstacksize + 1)?;

    // Re-read ci_func after check_stack (stack may have reallocated, but
    // StackIdx is index-based so the value is still correct).
    let ci_func: StackIdx = state.call_info[ci_idx.as_usize()].func;

    let func_val = state.get_at(ci_func).clone();
    state.push(func_val);

    //      setobjs2s(L, L->top.p++, ci->func.p + i);
    //      setnilvalue(s2v(ci->func.p + i));  /* erase original parameter (for GC) */
    //    }
    for i in 1..=nfixparams {
        // TODO(port): StackIdx is u32; if ci_func + i overflows the u32 range
        // this panics in debug.  In practice `i` is small (≤ 255 params), but
        // add a saturating or checked add in Phase B.
        let src: StackIdx = ci_func + i as i32;
        let param_val = state.get_at(src).clone();
        state.push(param_val);
        state.set_at(src, LuaValue::Nil);
    }

    // TODO(port): `actual + 1` may be negative if `actual < -1` (malformed call);
    // casting to StackIdx (u32) would underflow.  In practice Lua guarantees
    // actual >= 0 at this point, but add a debug_assert in Phase B.
    let offset = (actual + 1) as i32;
    state.call_info[ci_idx.as_usize()].func = state.call_info[ci_idx.as_usize()].func + offset;
    state.call_info[ci_idx.as_usize()].top = state.call_info[ci_idx.as_usize()].top + offset;

    if state.global().lua_version == lua_types::LuaVersion::V55 {
        let base = state.call_info[ci_idx.as_usize()].func + 1;
        state.set_at(base + nfixparams, LuaValue::Nil);
    }

    build_legacy_arg_table(state, ci_idx, proto, nextra)?;

    debug_assert!(state.top_idx().0 <= state.call_info[ci_idx.as_usize()].top.0);
    Ok(())
}

/// Build the Lua 5.1 implicit `arg` table at call entry, mirroring C 5.1's
/// `luaD_precall`, which fills `arg` inside `adjustvarargs` *before* any call or
/// line hook fires.
///
/// Our architecture defers the `arg` materialization to a `VARARGPACK` body
/// opcode, which runs after `OP_VARARGPREP` and therefore after the call hook.
/// A hook (or a `debug.getlocal` from a frame above) would then observe `arg`
/// as nil. Building it here restores the C ordering: by the time the call hook
/// runs, the frame already holds a populated `arg`. The body `VARARGPACK` later
/// rebuilds it idempotently for the non-hook path.
///
/// Only applies to Lua 5.1, only when the proto's first `VARARGPACK` carries the
/// K bit set. When the body uses `...` directly the parser rewrites that entry
/// `VARARGPACK` into a `LOADNIL` (see `clear_arg_table_needed`), so no K-bit
/// `VARARGPACK` survives, `legacy_arg_table_reg` returns `None`, and we build
/// nothing here — mirroring stock 5.1, which leaves `arg` declared but nil.
fn build_legacy_arg_table(
    state: &mut LuaState,
    ci_idx: CallInfoIdx,
    proto: &GcRef<lua_types::LuaProto>,
    nextra: i32,
) -> Result<(), LuaError> {
    if state.global().lua_version != lua_types::LuaVersion::V51 {
        return Ok(());
    }
    let Some(arg_reg) = legacy_arg_table_reg(proto) else {
        return Ok(());
    };

    let base = state.call_info[ci_idx.as_usize()].func + 1;
    let ra = base + arg_reg as i32;
    let ci_func = base - 1;

    let t = if nextra > 0 {
        state.new_table_with_sizes(nextra as u32, 1)?
    } else {
        state.new_table()
    };
    for k in 0..nextra {
        let src: StackIdx = ci_func - nextra + k;
        let val = state.get_at(src);
        t.raw_set_int(state, (k + 1) as i64, val)?;
    }
    let n_key = state.intern_str(b"n")?;
    t.raw_set(state, LuaValue::Str(n_key), LuaValue::Int(nextra as i64))?;
    state.set_at(ra, LuaValue::Table(t));
    Ok(())
}

/// Return the register of the Lua 5.1 implicit `arg` table, or `None` when the
/// proto should not build one at entry. The signal is the first `VARARGPACK`
/// instruction with the K bit set (see `build_legacy_arg_table`).
fn legacy_arg_table_reg(proto: &GcRef<lua_types::LuaProto>) -> Option<u8> {
    for inst in proto.code.iter() {
        if inst.opcode() == OpCode::VarArgPack {
            if inst.test_k() {
                return Some(inst.arg_a() as u8);
            }
            return None;
        }
    }
    None
}

// ── luaT_getvarargs ──────────────────────────────────────────────────────────

/// Copy vararg values into the stack starting at `where_idx`.
///
/// `wanted` specifies how many values to copy.  Pass `wanted < 0` (the
/// `LUA_MULTRET` convention) to request all available extra arguments; the
/// stack top is then set to `where_idx + nextra`.
///
/// Slots beyond `nextra` but within `wanted` are filled with `LuaValue::Nil`.
pub(crate) fn get_varargs(
    state: &mut LuaState,
    ci_idx: CallInfoIdx,
    where_idx: StackIdx,
    wanted: i32,
) -> Result<(), LuaError> {
    // Lua 5.5 named varargs (`function f(...t)`): `...` unpacks live from the
    // shared table `t` (its `n` field is the count), so mutating `t` is
    // observable through a later `...`. See `LuaProto.vararg_table_reg`.
    let vatab_reg = state
        .ci_lua_closure(ci_idx)
        .and_then(|cl| cl.proto.vararg_table_reg);
    if let Some(reg) = vatab_reg {
        let base = state.ci_base(ci_idx);
        if let LuaValue::Table(t) = state.get_at(base + reg as i32) {
            let n_key = state.intern_str(b"n")?;
            let nextra: i32 = match t.get(&LuaValue::Str(n_key)) {
                LuaValue::Int(i) if i >= 0 && i <= (i32::MAX / 2) as i64 => i as i32,
                _ => {
                    return Err(LuaError::runtime(format_args!(
                        "vararg table has no proper 'n'"
                    )));
                }
            };
            let wanted: i32 = if wanted < 0 {
                state.set_top(state.call_info[ci_idx.as_usize()].top);
                state.check_stack(nextra)?;
                state.gc_pre_collect_clear();
                state.gc().check_step();
                state.set_top(where_idx + nextra);
                nextra
            } else {
                wanted
            };
            let copy_count = wanted.min(nextra);
            for i in 0..copy_count {
                let val = t.get(&LuaValue::Int((i + 1) as i64));
                state.set_at(where_idx + i as i32, val);
            }
            for i in copy_count..wanted {
                state.set_at(where_idx + i as i32, LuaValue::Nil);
            }
            return Ok(());
        }
    }
    let nextra: i32 = state.call_info[ci_idx.as_usize()].nextra_args();

    //      wanted = nextra;  /* get all extra arguments available */
    //      checkstackGCp(L, nextra, where);  /* ensure stack space */
    //      L->top.p = where + nextra;  /* next instruction will need top */
    //    }
    let wanted: i32 = if wanted < 0 {
        state.set_top(state.call_info[ci_idx.as_usize()].top);
        state.check_stack(nextra)?;
        state.gc_pre_collect_clear();
        state.gc().check_step();
        // TODO(port): `where_idx + nextra as i32` may overflow if nextra
        // is very large; checked add in Phase B.
        state.set_top(where_idx + nextra as i32);
        nextra
    } else {
        wanted
    };

    //      setobjs2s(L, where + i, ci->func.p - nextra + i);
    //
    // After adjustvarargs, the extra args live at positions
    // ci->func - nextra .. ci->func - 1.
    let ci_func: StackIdx = state.call_info[ci_idx.as_usize()].func;
    let copy_count = wanted.min(nextra);
    for i in 0..copy_count {
        // TODO(port): subtraction on StackIdx (u32) underflows if nextra > ci_func.
        // Invariant: ci_func >= nextra (enforced by adjustvarargs), but add
        // a debug_assert in Phase B.
        let src: StackIdx = ci_func - nextra as i32 + i as i32;
        let val = state.get_at(src).clone();
        state.set_at(where_idx + i as i32, val);
    }

    //      setnilvalue(s2v(where + i));
    for i in copy_count..wanted {
        state.set_at(where_idx + i as i32, LuaValue::Nil);
    }
    Ok(())
}

// ──────────────────────────────────────────────────────────────────────────────
// PORT STATUS
//   source:        src/ltm.c  (271 lines, 15 functions)
//                  src/ltm.h  (104 lines; merged into this file)
//   target_crate:  lua-vm
//   confidence:    medium
//   todos:         10
//   port_notes:    7
//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
//   t2c2:          the two nextraargs sites that pattern-matched
//                  CallInfoFrame::Lua now use the set_nextra_args / nextra_args
//                  accessors on the flattened CallInfoFrame struct (former
//                  phase-b TODO resolved).
//   notes:
//     Logic translation is faithful; the main uncertainties are:
//     (1) GcRef<LuaTable> accessor pattern (.metatable() / .borrow().metatable)
//         is TBD — annotated TODO(port) in get_tm_by_obj.
//     (2) call_tm / call_tm_res use a `do_call` / `do_call_no_yield` naming
//         convention that must be confirmed against the LuaState API in Phase B.
//     (3) call_order_tm uses `top_idx()` as a scratch slot matching C's
//         EXTRA_STACK convention — annotated with TODO(port) for Phase B review.
//     (4) StackIdx (u32) arithmetic in adjust_varargs / get_varargs can
//         underflow — annotated TODO(port); add checked arithmetic in Phase B.
//     (5) PERF(port) callout for obj_type_name Vec alloc retired: the
//         allocation-free `obj_type_name_cow` is now the canonical
//         implementation; `obj_type_name` is a compat wrapper.  Existing
//         callers can migrate to `obj_type_name_cow` to avoid the
//         `.into_owned()` allocation.
//     (6) LUA_COMPAT_LT_LE block in call_order_tm omitted per PORTING.md §13.
//     (7) intern_str fallibility in obj_type_name resolved: obj_type_name_cow
//         uses get_str_bytes (infallible) and obj_type_name wraps it with
//         Ok(...), so no OOM propagation risk remains.
//     (8) luaC_fix in init() is stubbed as gc().fix_object() — no-op Phase A-C.
//     (9) #139: call_order_tm gates V51 mixed-Lua-type order comparisons to raise
//         luaG_ordererror before any TM lookup, matching 5.1's `ttype(l) !=
//         ttype(r)` guard. 5.2+ keep consulting the TM for mixed types. The check
//         is on the Lua type tag (base_type), so Int/Float (both `Number`) and the
//         two userdata kinds compare as C's `ttype` does.
//    (10) #events51: get_comp_tm_51 implements 5.1's get_compTM/get_equalTM
//         same-reference rule — the `__lt`/`__le`/`__eq` metamethod fires only
//         when both operands resolve to the identical handler function. For
//         ordered comparison call_order_tm raises luaG_ordererror in the
//         differing/missing-handler case (V51 only); equality (vm::equal_obj)
//         returns "not equal". 5.2+ consult left-then-right unconditionally.
// ──────────────────────────────────────────────────────────────────────────────
