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_gettmbyobj ──────────────────────────────────────────────────────────
200
201/// Look up a metamethod for any Lua value by dispatching on its type.
202///
203/// Tables and full userdata have per-object metatables; all other types use
204/// the per-type metatables on `GlobalState`. Returns `LuaValue::Nil` when
205/// neither the object nor its type has a metatable, or when the metatable
206/// does not contain the requested metamethod.
207pub(crate) fn get_tm_by_obj(
208 state: &mut LuaState,
209 o: &LuaValue,
210 event: TagMethod,
211) -> LuaValue {
212 // case LUA_TTABLE: mt = hvalue(o)->metatable; break;
213 // case LUA_TUSERDATA: mt = uvalue(o)->metatable; break;
214 // default: mt = G(L)->mt[ttype(o)];
215 // }
216 //
217 // TODO(port): `GcRef<LuaTable>` access pattern (direct field vs borrow) is
218 // TBD pending the GcRef/RefCell decision in Phase B; using `.metatable()`
219 // accessor here as a placeholder.
220 let mt: Option<GcRef<lua_types::value::LuaTable>> = match o {
221 LuaValue::Table(t) => t.metatable(),
222 LuaValue::UserData(u) => u.metatable(),
223 _ => {
224 let type_idx = o.base_type() as usize;
225 state.global().mt[type_idx].clone()
226 }
227 };
228
229 match mt {
230 Some(mt_ref) => {
231 // Clone the name string before the table lookup to avoid borrow conflict.
232 let ename = state.global().tmname[event as usize].clone();
233 mt_ref.get_short_str(&ename)
234 }
235 None => LuaValue::Nil,
236 }
237}
238
239// ── luaT_objtypename ─────────────────────────────────────────────────────────
240
241/// Return the human-readable type name for a Lua value without any heap
242/// allocation in the common case.
243///
244/// For tables and full userdata whose metatable defines `__name` as a string,
245/// returns `Cow::Owned` with the custom name bytes. For every other case
246/// returns `Cow::Borrowed` pointing into the static `TYPE_NAMES` table —
247/// no allocation, no interning, no `LuaState` access required.
248///
249/// PORT NOTE: C returns `const char*` — either a pointer into a GC-managed
250/// `LuaString` or a pointer into the static `luaT_typenames_` array. Rust
251/// models this as `Cow<'static, [u8]>`: `Borrowed` for static names,
252/// `Owned` only when the metatable `__name` field overrides the default.
253/// Uses `LuaTable::get_str_bytes` (linear byte-scan) instead of
254/// `intern_str` + `get_short_str` so the lookup is infallible and requires
255/// no mutable state access.
256pub(crate) fn obj_type_name_cow(o: &LuaValue) -> Cow<'static, [u8]> {
257 if matches!(o, LuaValue::LightUserData(_)) {
258 return Cow::Borrowed(b"light userdata");
259 }
260 // (ttisfulluserdata(o) && (mt = uvalue(o)->metatable) != NULL))
261 let mt: Option<GcRef<lua_types::value::LuaTable>> = match o {
262 LuaValue::Table(t) => t.metatable(),
263 LuaValue::UserData(u) => u.metatable(),
264 _ => None,
265 };
266 if let Some(mt_ref) = mt {
267 // Uses get_str_bytes (raw byte scan) rather than intern_str + get_short_str
268 // so no mutable state is needed and no error can propagate.
269 let name_val = mt_ref.get_str_bytes(b"__name");
270 if let LuaValue::Str(s) = name_val {
271 return Cow::Owned(s.as_bytes().to_vec());
272 }
273 }
274 Cow::Borrowed(type_name(o.base_type()))
275}
276
277/// Compatibility wrapper returning `Vec<u8>` for callers that have not yet
278/// migrated to `obj_type_name_cow`. Always allocates; prefer
279/// `obj_type_name_cow` for allocation-free lookup in error-path code.
280///
281/// PORT NOTE: `state` parameter retained for API compatibility; it is no
282/// longer used since the implementation delegates to `obj_type_name_cow`.
283/// Fallibility (`Result`) is also retained for the same reason.
284pub(crate) fn obj_type_name(_state: &mut LuaState, o: &LuaValue) -> Result<Vec<u8>, LuaError> {
285 Ok(obj_type_name_cow(o).into_owned())
286}
287
288// ── luaT_callTM ──────────────────────────────────────────────────────────────
289
290// const TValue *p2, const TValue *p3)
291/// Call tag method `f` with three arguments, discarding any return values.
292///
293/// Used for metamethods like `__gc` and `__close` that take three operands
294/// and whose return value is irrelevant.
295///
296/// If the current call frame is Lua bytecode (`isLuacode`), the metamethod
297/// may yield; otherwise yielding is suppressed (`callnoyield`).
298pub(crate) fn call_tm(
299 state: &mut LuaState,
300 f: LuaValue,
301 p1: LuaValue,
302 p2: LuaValue,
303 p3: LuaValue,
304) -> Result<(), LuaError> {
305 let func = state.top_idx();
306 //
307 // PORT NOTE: In C these are direct writes into the EXTRA_STACK reserve
308 // area above the official top. In Rust we use push() which manages
309 // capacity; the semantic result is identical.
310 state.push(f);
311 state.push(p1);
312 state.push(p2);
313 state.push(p3);
314 // luaD_call(L, func, 0);
315 // else
316 // luaD_callnoyield(L, func, 0);
317 //
318 // TODO(port): `do_call(func, nresults)` vs `call_from(func, nresults)` —
319 // exact Rust API name TBD; nargs is implicit as `top - func - 1`.
320 if state.current_ci().is_lua_code() {
321 state.do_call(func, 0)?;
322 } else {
323 state.do_call_no_yield(func, 0)?;
324 }
325 Ok(())
326}
327
328// ── luaT_callTMres ───────────────────────────────────────────────────────────
329
330// const TValue *p2, StkId res)
331/// Call tag method `f` with two arguments, writing the single result into
332/// the stack slot at index `res`.
333///
334/// `res` is a `StackIdx` (index-stable across stack reallocation) that must
335/// refer to a pre-existing or scratch slot. After return, the stack top is
336/// back to what it was before the call (i.e. `top == res`).
337///
338/// C uses `savestack`/`restorestack` (byte-offset from base) to preserve `res`
339/// across potential stack reallocations inside `luaD_call`. In Rust, `StackIdx`
340/// is already an index and needs no save/restore.
341pub(crate) fn call_tm_res(
342 state: &mut LuaState,
343 f: LuaValue,
344 p1: LuaValue,
345 p2: LuaValue,
346 res: StackIdx,
347) -> Result<(), LuaError> {
348 // savestack → StackIdx is already the stable byte-offset analogue; no-op.
349
350 let func = state.top_idx();
351 state.push(f);
352 state.push(p1);
353 state.push(p2);
354
355 // luaD_call(L, func, 1);
356 // else
357 // luaD_callnoyield(L, func, 1);
358 //
359 // TODO(port): same `do_call` API question as in call_tm above.
360 if state.current_ci().is_lua_code() {
361 state.do_call(func, 1)?;
362 } else {
363 state.do_call_no_yield(func, 1)?;
364 }
365
366 // restorestack → StackIdx is already stable; `res` is unchanged.
367
368 // Pre-decrement top, then copy that slot to res.
369 let result_val = state.pop();
370 state.set_at(res, result_val);
371 Ok(())
372}
373
374// ── callbinTM (static) ────────────────────────────────────────────────────────
375
376// StkId res, TMS event)
377/// Try to find and call a binary tag method for `event`.
378///
379/// Checks `p1` first, then `p2`, for a metamethod. If neither has one,
380/// returns `false` and leaves `res` unmodified. If found, calls it with
381/// `(p1, p2)` as arguments, writes the result to slot `res`, and returns `true`.
382fn call_bin_tm(
383 state: &mut LuaState,
384 p1: &LuaValue,
385 p2: &LuaValue,
386 res: StackIdx,
387 event: TagMethod,
388) -> Result<bool, LuaError> {
389 let tm = get_tm_by_obj(state, p1, event);
390 // tm = luaT_gettmbyobj(L, p2, event); /* try second operand */
391 let tm = if tm.is_nil() {
392 get_tm_by_obj(state, p2, event)
393 } else {
394 tm
395 };
396 if tm.is_nil() {
397 return Ok(false);
398 }
399 // Clone p1/p2 before the mutable borrow of `state` via call_tm_res.
400 call_tm_res(state, tm, p1.clone(), p2.clone(), res)?;
401 Ok(true)
402}
403
404// ── luaT_trybinTM ────────────────────────────────────────────────────────────
405
406// StkId res, TMS event)
407/// Attempt a binary metamethod call; raise a type error if no metamethod exists.
408///
409/// For bitwise operations, further distinguishes between:
410/// - Both operands are numbers but not integers → `LuaError::int_overflow`
411/// (`luaG_tointerror` — "number has no integer representation")
412/// - At least one operand is not a number → `LuaError::arith_error` with
413/// "perform bitwise operation on"
414///
415/// All other missing metamethods raise `LuaError::arith_error` with
416/// "perform arithmetic on".
417pub(crate) fn try_bin_tm(
418 state: &mut LuaState,
419 p1: &LuaValue,
420 p1_idx: Option<StackIdx>,
421 p2: &LuaValue,
422 p2_idx: Option<StackIdx>,
423 res: StackIdx,
424 event: TagMethod,
425) -> Result<(), LuaError> {
426 if !call_bin_tm(state, p1, p2, res, event)? {
427 // case TM_BAND: case TM_BOR: case TM_BXOR:
428 // case TM_SHL: case TM_SHR: case TM_BNOT: {
429 // if (ttisnumber(p1) && ttisnumber(p2))
430 // luaG_tointerror(L, p1, p2);
431 // else
432 // luaG_opinterror(L, p1, p2, "perform bitwise operation on");
433 // }
434 // /* calls never return, but to avoid warnings: *//* FALLTHROUGH */
435 // default:
436 // luaG_opinterror(L, p1, p2, "perform arithmetic on");
437 // }
438 //
439 // PORT NOTE: the C switch has a dead "FALLTHROUGH" for bitwise cases
440 // because both branches of the inner if/else call noreturn functions.
441 // In Rust `match` has no implicit fallthrough; each arm is self-contained
442 // and explicitly returns `Err(...)`.
443 match event {
444 TagMethod::BAnd
445 | TagMethod::BOr
446 | TagMethod::BXor
447 | TagMethod::Shl
448 | TagMethod::Shr
449 | TagMethod::BNot => {
450 if matches!(p1, LuaValue::Int(_) | LuaValue::Float(_))
451 && matches!(p2, LuaValue::Int(_) | LuaValue::Float(_))
452 {
453 // "(field 'huge')" / "(local 'x')" etc. based on the bytecode that
454 // produced the bad operand.
455 return Err(crate::debug::to_int_error(state, p1, p1_idx, p2, p2_idx));
456 } else {
457 // varinfo on the non-number operand.
458 let p1_idx = p1_idx.unwrap_or(StackIdx(0));
459 let p2_idx = p2_idx.unwrap_or(StackIdx(0));
460 return Err(crate::debug::op_int_error(
461 state, p1, p1_idx, p2, p2_idx, b"perform bitwise operation on",
462 ));
463 }
464 }
465 _ => {
466 // varinfo enriches with "(global 'aaa')" etc.
467 let p1_idx = p1_idx.unwrap_or(StackIdx(0));
468 let p2_idx = p2_idx.unwrap_or(StackIdx(0));
469 return Err(crate::debug::op_int_error(
470 state, p1, p1_idx, p2, p2_idx, b"perform arithmetic on",
471 ));
472 }
473 }
474 }
475 Ok(())
476}
477
478// ── luaT_tryconcatTM ─────────────────────────────────────────────────────────
479
480/// Attempt the `__concat` metamethod on the two values at the top of the stack.
481///
482/// Reads `stack[top-2]` and `stack[top-1]`, searches both for `__concat`,
483/// calls it with `(stack[top-2], stack[top-1])` writing the result back to
484/// `stack[top-2]`, or raises `LuaError::concat_error` if no metamethod exists.
485pub(crate) fn try_concat_tm(state: &mut LuaState) -> Result<(), LuaError> {
486 let top = state.top_idx();
487 // luaG_concaterror(L, s2v(top - 2), s2v(top - 1));
488 //
489 // Clone the operands before any call that might mutate the stack.
490 let p1 = state.get_at(top - 2).clone();
491 let p2 = state.get_at(top - 1).clone();
492 if !call_bin_tm(state, &p1, &p2, top - 2, TagMethod::Concat)? {
493 return Err(LuaError::concat_error(&p1, &p2));
494 }
495 Ok(())
496}
497
498// ── luaT_trybinassocTM ───────────────────────────────────────────────────────
499
500// int flip, StkId res, TMS event)
501/// Try a binary associative metamethod, optionally swapping the operands.
502///
503/// When `flip` is `true`, operands are exchanged before dispatch. This
504/// implements Lua's symmetry rule: if `a OP b` finds no metamethod on `a`,
505/// the VM retries with `b OP a` (setting flip=true to restore the original
506/// argument order for the call).
507pub(crate) fn try_bin_assoc_tm(
508 state: &mut LuaState,
509 p1: &LuaValue,
510 p1_idx: Option<StackIdx>,
511 p2: &LuaValue,
512 p2_idx: Option<StackIdx>,
513 flip: bool,
514 res: StackIdx,
515 event: TagMethod,
516) -> Result<(), LuaError> {
517 // luaT_trybinTM(L, p2, p1, res, event);
518 // else
519 // luaT_trybinTM(L, p1, p2, res, event);
520 if flip {
521 try_bin_tm(state, p2, p2_idx, p1, p1_idx, res, event)
522 } else {
523 try_bin_tm(state, p1, p1_idx, p2, p2_idx, res, event)
524 }
525}
526
527// ── luaT_trybiniTM ───────────────────────────────────────────────────────────
528
529// int flip, StkId res, TMS event)
530/// Try a binary metamethod where the second operand is an integer constant.
531///
532/// Boxes `i2` as `LuaValue::Int` and delegates to `try_bin_assoc_tm`.
533pub(crate) fn try_bini_tm(
534 state: &mut LuaState,
535 p1: &LuaValue,
536 p1_idx: Option<StackIdx>,
537 i2: i64,
538 flip: bool,
539 res: StackIdx,
540 event: TagMethod,
541) -> Result<(), LuaError> {
542 let aux = LuaValue::Int(i2);
543 // The immediate operand has no stack location, so it gets `None`.
544 try_bin_assoc_tm(state, p1, p1_idx, &aux, None, flip, res, event)
545}
546
547// ── luaT_callorderTM ─────────────────────────────────────────────────────────
548
549// TMS event)
550/// Call an order metamethod (`__lt` or `__le`) and return its boolean result.
551///
552/// Returns `true` if the metamethod returned a truthy value.
553/// Raises `LuaError::order_error` if neither operand has the metamethod.
554///
555/// PORT NOTE: The `LUA_COMPAT_LT_LE` block (which falls back from `__le` to
556/// `!(p2 < p1)`) is omitted per PORTING.md §13 — no compatibility shims.
557pub(crate) fn call_order_tm(
558 state: &mut LuaState,
559 p1: &LuaValue,
560 p2: &LuaValue,
561 event: TagMethod,
562) -> Result<bool, LuaError> {
563 // return !l_isfalse(s2v(L->top.p));
564 //
565 // PORT NOTE: In C, `L->top.p` is used as a scratch slot (written by
566 // callTMres then immediately read) in the EXTRA_STACK reserved area above
567 // the official stack top — the stack top is NOT officially advanced.
568 // In Rust we pass `state.top_idx()` as `res`; call_bin_tm → call_tm_res
569 // pushes 3 values, calls, pops the result back to res, and leaves top ==
570 // res (i.e. top unchanged relative to entry). Reading get_at(res_idx)
571 // after this is safe because the slot was just written and top == res_idx.
572 //
573 // TODO(port): Verify in Phase B that no call path between call_bin_tm
574 // returning and the get_at read can disturb the scratch slot or reset top
575 // below res_idx. The invariant holds as long as do_call with 1 result
576 // leaves exactly one value on the stack above func.
577 let res_idx = state.top_idx();
578 if call_bin_tm(state, p1, p2, res_idx, event)? {
579 // l_isfalse(o) → matches!(o, LuaValue::Nil | LuaValue::Bool(false))
580 let result = state.get_at(res_idx).clone();
581 return Ok(!matches!(result, LuaValue::Nil | LuaValue::Bool(false)));
582 }
583
584 // PORT NOTE: LUA_COMPAT_LT_LE block skipped (see above).
585
586 Err(crate::debug::order_error(state, p1, p2))
587}
588
589// ── luaT_callorderiTM ────────────────────────────────────────────────────────
590
591// int flip, int isfloat, TMS event)
592/// Call an order metamethod where the second operand is a primitive int or float.
593///
594/// `v2` is a C `int`; `isfloat` selects whether it is coerced to
595/// `LuaValue::Float` (via `cast_num`) or kept as `LuaValue::Int`.
596/// When `flip` is true the operands are swapped so that `p1` was originally
597/// on the right-hand side.
598pub(crate) fn call_orderi_tm(
599 state: &mut LuaState,
600 p1: &LuaValue,
601 v2: i32,
602 flip: bool,
603 isfloat: bool,
604 event: TagMethod,
605) -> Result<bool, LuaError> {
606 // setfltvalue(&aux, cast_num(v2));
607 // }
608 // else
609 // setivalue(&aux, v2);
610 let aux = if isfloat {
611 LuaValue::Float(v2 as f64)
612 } else {
613 LuaValue::Int(v2 as i64)
614 };
615
616 // p2 = p1; p1 = &aux; /* correct them */
617 // }
618 // else
619 // p2 = &aux;
620 if flip {
621 call_order_tm(state, &aux, p1, event)
622 } else {
623 call_order_tm(state, p1, &aux, event)
624 }
625}
626
627// ── luaT_adjustvarargs ───────────────────────────────────────────────────────
628
629// const Proto *p)
630/// Adjust the stack layout for a vararg function at call entry.
631///
632/// Moves the fixed parameters above the extra (vararg) arguments and copies
633/// the function object alongside them so the function body sees its registers
634/// at the expected offsets. Records the extra-argument count in the CallInfo
635/// for `OP_VARARG` use.
636///
637/// Before call: `[func | fixed... | extra...]`
638/// After call: `[func | nil... | extra... | func′ | fixed...]`
639/// (`ci.func` and `ci.top` are advanced by `actual + 1`.)
640pub(crate) fn adjust_varargs(
641 state: &mut LuaState,
642 nfixparams: i32,
643 ci_idx: CallInfoIdx,
644 proto: &GcRef<lua_types::LuaProto>,
645) -> Result<(), LuaError> {
646 let ci_func: StackIdx = state.call_info[ci_idx.as_usize()].func;
647 let actual = (state.top_idx().0 as i32) - (ci_func.0 as i32) - 1;
648 let nextra = actual - nfixparams;
649 // TODO(phase-b): nextraargs lives inside CallInfoFrame::Lua; needs proper
650 // pattern-match write through state.call_info[..].u.
651 if let crate::state::CallInfoFrame::Lua { ref mut nextraargs, .. } = state.call_info[ci_idx.as_usize()].u {
652 *nextraargs = nextra;
653 }
654
655 let maxstacksize = proto.maxstacksize as i32;
656 state.check_stack(maxstacksize + 1)?;
657
658 // Re-read ci_func after check_stack (stack may have reallocated, but
659 // StackIdx is index-based so the value is still correct).
660 let ci_func: StackIdx = state.call_info[ci_idx.as_usize()].func;
661
662 let func_val = state.get_at(ci_func).clone();
663 state.push(func_val);
664
665 // setobjs2s(L, L->top.p++, ci->func.p + i);
666 // setnilvalue(s2v(ci->func.p + i)); /* erase original parameter (for GC) */
667 // }
668 for i in 1..=nfixparams {
669 // TODO(port): StackIdx is u32; if ci_func + i overflows the u32 range
670 // this panics in debug. In practice `i` is small (≤ 255 params), but
671 // add a saturating or checked add in Phase B.
672 let src: StackIdx = ci_func + i as i32;
673 let param_val = state.get_at(src).clone();
674 state.push(param_val);
675 state.set_at(src, LuaValue::Nil);
676 }
677
678 // TODO(port): `actual + 1` may be negative if `actual < -1` (malformed call);
679 // casting to StackIdx (u32) would underflow. In practice Lua guarantees
680 // actual >= 0 at this point, but add a debug_assert in Phase B.
681 let offset = (actual + 1) as i32;
682 state.call_info[ci_idx.as_usize()].func = state.call_info[ci_idx.as_usize()].func + offset;
683 state.call_info[ci_idx.as_usize()].top = state.call_info[ci_idx.as_usize()].top + offset;
684
685 debug_assert!(state.top_idx().0 <= state.call_info[ci_idx.as_usize()].top.0);
686 Ok(())
687}
688
689// ── luaT_getvarargs ──────────────────────────────────────────────────────────
690
691/// Copy vararg values into the stack starting at `where_idx`.
692///
693/// `wanted` specifies how many values to copy. Pass `wanted < 0` (the
694/// `LUA_MULTRET` convention) to request all available extra arguments; the
695/// stack top is then set to `where_idx + nextra`.
696///
697/// Slots beyond `nextra` but within `wanted` are filled with `LuaValue::Nil`.
698pub(crate) fn get_varargs(
699 state: &mut LuaState,
700 ci_idx: CallInfoIdx,
701 where_idx: StackIdx,
702 wanted: i32,
703) -> Result<(), LuaError> {
704 let nextra: i32 = if let crate::state::CallInfoFrame::Lua { nextraargs, .. } = state.call_info[ci_idx.as_usize()].u { nextraargs } else { 0 };
705
706 // wanted = nextra; /* get all extra arguments available */
707 // checkstackGCp(L, nextra, where); /* ensure stack space */
708 // L->top.p = where + nextra; /* next instruction will need top */
709 // }
710 let wanted: i32 = if wanted < 0 {
711 state.check_stack(nextra)?;
712 state.gc().check_step();
713 // TODO(port): `where_idx + nextra as i32` may overflow if nextra
714 // is very large; checked add in Phase B.
715 state.set_top(where_idx + nextra as i32);
716 nextra
717 } else {
718 wanted
719 };
720
721 // setobjs2s(L, where + i, ci->func.p - nextra + i);
722 //
723 // After adjustvarargs, the extra args live at positions
724 // ci->func - nextra .. ci->func - 1.
725 let ci_func: StackIdx = state.call_info[ci_idx.as_usize()].func;
726 let copy_count = wanted.min(nextra);
727 for i in 0..copy_count {
728 // TODO(port): subtraction on StackIdx (u32) underflows if nextra > ci_func.
729 // Invariant: ci_func >= nextra (enforced by adjustvarargs), but add
730 // a debug_assert in Phase B.
731 let src: StackIdx = ci_func - nextra as i32 + i as i32;
732 let val = state.get_at(src).clone();
733 state.set_at(where_idx + i as i32, val);
734 }
735
736 // setnilvalue(s2v(where + i));
737 for i in copy_count..wanted {
738 state.set_at(where_idx + i as i32, LuaValue::Nil);
739 }
740 Ok(())
741}
742
743// ──────────────────────────────────────────────────────────────────────────────
744// PORT STATUS
745// source: src/ltm.c (271 lines, 15 functions)
746// src/ltm.h (104 lines; merged into this file)
747// target_crate: lua-vm
748// confidence: medium
749// todos: 10
750// port_notes: 7
751// unsafe_blocks: 0 (must be 0 outside explicit unsafe-budget crates)
752// notes:
753// Logic translation is faithful; the main uncertainties are:
754// (1) GcRef<LuaTable> accessor pattern (.metatable() / .borrow().metatable)
755// is TBD — annotated TODO(port) in get_tm_by_obj.
756// (2) call_tm / call_tm_res use a `do_call` / `do_call_no_yield` naming
757// convention that must be confirmed against the LuaState API in Phase B.
758// (3) call_order_tm uses `top_idx()` as a scratch slot matching C's
759// EXTRA_STACK convention — annotated with TODO(port) for Phase B review.
760// (4) StackIdx (u32) arithmetic in adjust_varargs / get_varargs can
761// underflow — annotated TODO(port); add checked arithmetic in Phase B.
762// (5) PERF(port) callout for obj_type_name Vec alloc retired: the
763// allocation-free `obj_type_name_cow` is now the canonical
764// implementation; `obj_type_name` is a compat wrapper. Existing
765// callers can migrate to `obj_type_name_cow` to avoid the
766// `.into_owned()` allocation.
767// (6) LUA_COMPAT_LT_LE block in call_order_tm omitted per PORTING.md §13.
768// (7) intern_str fallibility in obj_type_name resolved: obj_type_name_cow
769// uses get_str_bytes (infallible) and obj_type_name wraps it with
770// Ok(...), so no OOM propagation risk remains.
771// (8) luaC_fix in init() is stubbed as gc().fix_object() — no-op Phase A-C.
772// ──────────────────────────────────────────────────────────────────────────────