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