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