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// ──────────────────────────────────────────────────────────────────────────────