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