Skip to main content

lua_stdlib/
table_lib.rs

1//! Rust port of `ltablib.c` — Lua `table` standard library.
2//!
3//! Provides: `table.concat`, `table.insert`, `table.move`, `table.pack`,
4//! `table.remove`, `table.sort`, `table.unpack`.
5//!
6//! C source: `reference/lua-5.4.7/src/ltablib.c` (430 lines, 14 functions)
7
8use lua_types::{GcRef, LuaError, LuaTable, LuaType, LuaValue};
9use crate::state_stub::{LuaState, LuaStateStubExt as _, CompareOp};
10
11// ─── Operation flags ──────────────────────────────────────────────────────────
12const TAB_R: u32 = 1;
13const TAB_W: u32 = 2;
14const TAB_L: u32 = 4;
15const TAB_RW: u32 = TAB_R | TAB_W;
16
17const RANLIMIT: u32 = 100;
18
19type IdxT = u32;
20
21// ─── Internal helpers ─────────────────────────────────────────────────────────
22
23/// currently sitting at stack depth `n`; returns `true` if the result is not nil.
24///
25/// ```c
26/// static int checkfield (lua_State *L, const char *key, int n) {
27///   lua_pushstring(L, key);
28///   return (lua_rawget(L, -n) != LUA_TNIL);
29/// }
30/// ```
31fn check_field(state: &mut LuaState, key: &[u8], n: i32) -> Result<bool, LuaError> {
32    // TODO(port): state.push_string pushes a Lua string from &[u8]; verify method name
33    state.push_string(key)?;
34    // raw_get(-n): looks up MT[key] (MT is at -n after the key push), replaces key with value
35    let ty = state.raw_get(-n)?;
36    Ok(ty != LuaType::Nil)
37}
38
39/// metatable with the metamethods required by `what`
40/// (`TAB_R` → `__index`, `TAB_W` → `__newindex`, `TAB_L` → `__len`).
41///
42/// ```c
43/// static void checktab (lua_State *L, int arg, int what) {
44///   if (lua_type(L, arg) != LUA_TTABLE) {
45///     int n = 1;
46///     if (lua_getmetatable(L, arg) &&
47///         (!(what & TAB_R) || checkfield(L, "__index", ++n)) &&
48///         (!(what & TAB_W) || checkfield(L, "__newindex", ++n)) &&
49///         (!(what & TAB_L) || checkfield(L, "__len", ++n))) {
50///       lua_pop(L, n);
51///     }
52///     else
53///       luaL_checktype(L, arg, LUA_TTABLE);
54///   }
55/// }
56/// ```
57///
58/// PORT NOTE: stack cleanup on the error path is elided here (in C, `longjmp`
59/// unwinds automatically). Phase B should add cleanup before the `check_arg_type`
60/// call to leave the stack consistent.
61fn check_tab(state: &mut LuaState, arg: i32, what: u32) -> Result<(), LuaError> {
62    if state.type_at(arg) == LuaType::Table {
63        return Ok(());
64    }
65    // `n` tracks how many items have been pushed (MT + checked field values).
66    let mut n: i32 = 1;
67    // TODO(port): state.get_metatable returns bool (pushes MT if found); verify method name
68    let has_mt = state.get_metatable(arg)?;
69    let mut ok = has_mt;
70
71    // Short-circuit: each field is only checked if all previous checks passed.
72    if ok && (what & TAB_R) != 0 {
73        n += 1;
74        ok = check_field(state, b"__index", n)?;
75    }
76    if ok && (what & TAB_W) != 0 {
77        n += 1;
78        ok = check_field(state, b"__newindex", n)?;
79    }
80    if ok && (what & TAB_L) != 0 {
81        n += 1;
82        ok = check_field(state, b"__len", n)?;
83    }
84
85    if ok {
86        state.pop_n(n as usize);
87        Ok(())
88    } else {
89        state.check_arg_type(arg, LuaType::Table)
90    }
91}
92
93///
94/// Check that argument `n` is a table (or table-like per `w`) and return its length.
95fn aux_getn(state: &mut LuaState, n: i32, w: u32) -> Result<i64, LuaError> {
96    check_tab(state, n, w | TAB_L)?;
97    // TODO(port): state.length_at applies the `#` operator and returns i64; verify method name
98    state.length_at(n)
99}
100
101#[inline]
102fn plain_table_at(state: &mut LuaState, idx: i32) -> Option<GcRef<LuaTable>> {
103    match state.value_at(idx) {
104        LuaValue::Table(tbl) if tbl.metatable().is_none() => Some(tbl),
105        _ => None,
106    }
107}
108
109#[inline]
110fn raw_set_int(
111    state: &mut LuaState,
112    tbl: GcRef<LuaTable>,
113    key: i64,
114    value: LuaValue,
115) -> Result<(), LuaError> {
116    state.gc_barrier_back(LuaValue::Table(tbl), value);
117    tbl.try_raw_set_int(key, value)
118}
119
120// ─── table.insert ─────────────────────────────────────────────────────────────
121
122///
123/// ```c
124/// static int tinsert (lua_State *L) {
125///   lua_Integer pos;
126///   lua_Integer e = aux_getn(L, 1, TAB_RW);
127///   e = luaL_intop(+, e, 1);
128///   switch (lua_gettop(L)) {
129///     case 2: { pos = e; break; }
130///     case 3: {
131///       lua_Integer i;
132///       pos = luaL_checkinteger(L, 2);
133///       luaL_argcheck(L, (lua_Unsigned)pos - 1u < (lua_Unsigned)e, 2,
134///                        "position out of bounds");
135///       for (i = e; i > pos; i--) {
136///         lua_geti(L, 1, i - 1);
137///         lua_seti(L, 1, i);
138///       }
139///       break;
140///     }
141///     default:
142///       return luaL_error(L, "wrong number of arguments to 'insert'");
143///   }
144///   lua_seti(L, 1, pos);
145///   return 0;
146/// }
147/// ```
148pub fn insert(state: &mut LuaState) -> Result<usize, LuaError> {
149    let mut e = aux_getn(state, 1, TAB_RW)?;
150    e = (e as u64).wrapping_add(1) as i64;
151    let plain_table = plain_table_at(state, 1);
152
153    let pos: i64 = match state.get_top() {
154        2 => {
155            if let Some(tbl) = plain_table {
156                let value = state.value_at(2);
157                raw_set_int(state, tbl, e, value)?;
158                state.pop_n(1);
159                return Ok(0);
160            }
161            e
162        }
163        3 => {
164            let pos = state.check_arg_integer(2)?;
165            // Checks 1 <= pos <= e (wrapping subtraction catches pos <= 0)
166            if !((pos as u64).wrapping_sub(1) < (e as u64)) {
167                return Err(lua_vm::debug::arg_error_impl(state, 2, b"position out of bounds"));
168            }
169            if let Some(tbl) = plain_table {
170                let value = state.value_at(3);
171                let mut i = e;
172                while i > pos {
173                    let shifted = tbl.get_int(i - 1);
174                    raw_set_int(state, tbl, i, shifted)?;
175                    i -= 1;
176                }
177                raw_set_int(state, tbl, pos, value)?;
178                state.pop_n(1);
179                return Ok(0);
180            }
181            // Cache the table once to avoid re-resolving stack slot 1 on every
182            // iteration of the shift loop. C's lua_geti is a single pointer
183            // arithmetic operation; our index_to_value is a function call with
184            // branches, so this saves ~2N index resolutions for shift count N.
185            let tbl = state.value_at(1);
186            let mut i = e;
187            while i > pos {
188                state.table_get_i_value(&tbl, i - 1)?;
189                state.table_set_i_value(&tbl, i)?;
190                i -= 1;
191            }
192            pos
193        }
194        _ => {
195            return Err(LuaError::runtime(format_args!(
196                "wrong number of arguments to 'insert'"
197            )));
198        }
199    };
200    state.table_set_i(1, pos)?;
201    Ok(0)
202}
203
204// ─── table.remove ─────────────────────────────────────────────────────────────
205
206///
207/// ```c
208/// static int tremove (lua_State *L) {
209///   lua_Integer size = aux_getn(L, 1, TAB_RW);
210///   lua_Integer pos = luaL_optinteger(L, 2, size);
211///   if (pos != size)
212///     luaL_argcheck(L, (lua_Unsigned)pos - 1u <= (lua_Unsigned)size, 2,
213///                      "position out of bounds");
214///   lua_geti(L, 1, pos);
215///   for ( ; pos < size; pos++) {
216///     lua_geti(L, 1, pos + 1);
217///     lua_seti(L, 1, pos);
218///   }
219///   lua_pushnil(L);
220///   lua_seti(L, 1, pos);
221///   return 1;
222/// }
223/// ```
224pub fn remove(state: &mut LuaState) -> Result<usize, LuaError> {
225    let size = aux_getn(state, 1, TAB_RW)?;
226    let mut pos = state.opt_arg_integer(2, size)?;
227    if pos != size {
228        if !((pos as u64).wrapping_sub(1) <= (size as u64)) {
229            let argn = if state.global().lua_version == lua_types::LuaVersion::V53 { 1 } else { 2 };
230            return Err(lua_vm::debug::arg_error_impl(state, argn, b"position out of bounds"));
231        }
232    }
233    // Cache the table once to avoid re-resolving stack slot 1 on every
234    // iteration of the shift loop. C's lua_geti is a single pointer
235    // arithmetic operation; our index_to_value is a function call with
236    // branches, so this saves ~2N index resolutions for shift count N.
237    if let Some(tbl) = plain_table_at(state, 1) {
238        let result = tbl.get_int(pos);
239        state.push(result);
240        while pos < size {
241            let shifted = tbl.get_int(pos + 1);
242            raw_set_int(state, tbl, pos, shifted)?;
243            pos += 1;
244        }
245        raw_set_int(state, tbl, pos, LuaValue::Nil)?;
246        return Ok(1);
247    }
248    let tbl = state.value_at(1);
249    state.table_get_i_value(&tbl, pos)?;   // push element to be returned
250    while pos < size {
251        state.table_get_i_value(&tbl, pos + 1)?;
252        state.table_set_i_value(&tbl, pos)?;
253        pos += 1;
254    }
255    state.push(LuaValue::Nil);
256    state.table_set_i_value(&tbl, pos)?;   // remove last slot (table[pos] = nil)
257    Ok(1)
258}
259
260// ─── table.move ───────────────────────────────────────────────────────────────
261
262///
263/// Copies elements `a1[f..e]` into `a2[t..]` (or `a1[t..]` if `a2` is absent).
264/// Copies in increasing order when safe, decreasing when ranges overlap.
265///
266/// ```c
267/// static int tmove (lua_State *L) {
268///   lua_Integer f = luaL_checkinteger(L, 2);
269///   lua_Integer e = luaL_checkinteger(L, 3);
270///   lua_Integer t = luaL_checkinteger(L, 4);
271///   int tt = !lua_isnoneornil(L, 5) ? 5 : 1;
272///   checktab(L, 1, TAB_R);
273///   checktab(L, tt, TAB_W);
274///   if (e >= f) {
275///     lua_Integer n, i;
276///     luaL_argcheck(L, f > 0 || e < LUA_MAXINTEGER + f, 3, "too many elements to move");
277///     n = e - f + 1;
278///     luaL_argcheck(L, t <= LUA_MAXINTEGER - n + 1, 4, "destination wrap around");
279///     if (t > e || t <= f || (tt != 1 && !lua_compare(L, 1, tt, LUA_OPEQ))) {
280///       for (i = 0; i < n; i++) { lua_geti(L, 1, f + i); lua_seti(L, tt, t + i); }
281///     } else {
282///       for (i = n - 1; i >= 0; i--) { lua_geti(L, 1, f + i); lua_seti(L, tt, t + i); }
283///     }
284///   }
285///   lua_pushvalue(L, tt);
286///   return 1;
287/// }
288/// ```
289pub fn tmove(state: &mut LuaState) -> Result<usize, LuaError> {
290    let f = state.check_arg_integer(2)?;
291    let e = state.check_arg_integer(3)?;
292    let t = state.check_arg_integer(4)?;
293    let tt: i32 = if !matches!(state.type_at(5), LuaType::None | LuaType::Nil) {
294        5
295    } else {
296        1
297    };
298    check_tab(state, 1, TAB_R)?;
299    check_tab(state, tt, TAB_W)?;
300
301    if e >= f {
302        if !(f > 0 || e < i64::MAX + f) {
303            return Err(lua_vm::debug::arg_error_impl(state, 3, b"too many elements to move"));
304        }
305        let n = e - f + 1;
306        if !(t <= i64::MAX - n + 1) {
307            return Err(lua_vm::debug::arg_error_impl(state, 4, b"destination wrap around"));
308        }
309        // Copy forward (increasing) when safe to do so; backward when ranges overlap.
310        // TODO(port): state.compare(a, b, CompareOp::Eq) → lua_compare LUA_OPEQ; verify method
311        let copy_forward = t > e
312            || t <= f
313            || (tt != 1 && !state.compare(1, tt, CompareOp::Eq)?);
314        if copy_forward {
315            for i in 0..n {
316                state.table_get_i(1, f + i)?;
317                state.table_set_i(tt, t + i)?;
318            }
319        } else {
320            for i in (0..n).rev() {
321                state.table_get_i(1, f + i)?;
322                state.table_set_i(tt, t + i)?;
323            }
324        }
325    }
326    // TODO(port): state.push_value_at → lua_pushvalue; verify method name
327    state.push_value_at(tt)?;
328    Ok(1)
329}
330
331// ─── table.concat ─────────────────────────────────────────────────────────────
332
333/// a string-or-number, add its string representation to `buf`, then pop it.
334///
335/// ```c
336/// static void addfield (lua_State *L, luaL_Buffer *b, lua_Integer i) {
337///   lua_geti(L, 1, i);
338///   if (l_unlikely(!lua_isstring(L, -1)))
339///     luaL_error(L, "invalid value (%s) at index %I in table for 'concat'",
340///                   luaL_typename(L, -1), (LUAI_UACINT)i);
341///   luaL_addvalue(b);
342/// }
343/// ```
344///
345/// PORT NOTE: `luaL_Buffer` in C accumulates bytes and then pushes the result;
346/// Rust uses a `Vec<u8>` accumulator passed by mutable reference instead.
347fn add_field(state: &mut LuaState, buf: &mut Vec<u8>, idx: i64) -> Result<(), LuaError> {
348    state.table_get_i(1, idx)?;
349    if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
350        let type_name = state.type_name_str_at(-1);
351        let msg = format!(
352            "invalid value ({}) at index {} in table for 'concat'",
353            String::from_utf8_lossy(type_name),
354            idx
355        );
356        return crate::auxlib::lua_error(state, msg.as_bytes()).map(|_| ());
357    }
358    // TODO(port): state.to_bytes_at(-1) converts via Lua's tostring coercion; verify method name
359    let bytes = state.to_bytes_at(-1).ok_or_else(|| LuaError::runtime(format_args!("invalid value at index {}", idx)))?;
360    buf.extend_from_slice(&bytes);
361    state.pop_n(1);
362    Ok(())
363}
364
365///
366/// ```c
367/// static int tconcat (lua_State *L) {
368///   luaL_Buffer b;
369///   lua_Integer last = aux_getn(L, 1, TAB_R);
370///   size_t lsep;
371///   const char *sep = luaL_optlstring(L, 2, "", &lsep);
372///   lua_Integer i = luaL_optinteger(L, 3, 1);
373///   last = luaL_optinteger(L, 4, last);
374///   luaL_buffinit(L, &b);
375///   for (; i < last; i++) { addfield(L, &b, i); luaL_addlstring(&b, sep, lsep); }
376///   if (i == last) addfield(L, &b, i);
377///   luaL_pushresult(&b);
378///   return 1;
379/// }
380/// ```
381pub fn concat(state: &mut LuaState) -> Result<usize, LuaError> {
382    let last = aux_getn(state, 1, TAB_R)?;
383    // TODO(port): state.opt_arg_lstring(n, default) → luaL_optlstring; verify method name
384    // Clone the separator before any stack-mutating calls that might invalidate it.
385    let sep: Vec<u8> = state.opt_arg_lstring(2, Some(b""))?.unwrap_or_default();
386    let mut i = state.opt_arg_integer(3, 1)?;
387    let last = state.opt_arg_integer(4, last)?;
388
389    // PORT NOTE: C uses luaL_Buffer (which may back-patch the stack);
390    // Rust uses a plain Vec<u8> accumulator and pushes the result at the end.
391    let mut buf: Vec<u8> = Vec::new();
392    while i < last {
393        add_field(state, &mut buf, i)?;
394        buf.extend_from_slice(&sep);
395        i += 1;
396    }
397    if i == last {
398        add_field(state, &mut buf, i)?;
399    }
400    // TODO(port): state.push_lstring pushes a Lua string from &[u8]; verify method name
401    state.push_lstring(&buf)?;
402    Ok(1)
403}
404
405// ─── table.pack / table.unpack ────────────────────────────────────────────────
406
407///
408/// Creates a new table `t` with all arguments as integer keys and `t.n` set
409/// to the argument count.
410///
411/// ```c
412/// static int tpack (lua_State *L) {
413///   int i;
414///   int n = lua_gettop(L);
415///   lua_createtable(L, n, 1);
416///   lua_insert(L, 1);
417///   for (i = n; i >= 1; i--)
418///     lua_seti(L, 1, i);
419///   lua_pushinteger(L, n);
420///   lua_setfield(L, 1, "n");
421///   return 1;
422/// }
423/// ```
424pub fn pack(state: &mut LuaState) -> Result<usize, LuaError> {
425    let n = state.get_top();
426    // TODO(port): state.create_table(narr, nrec) → lua_createtable; verify method name
427    state.create_table(n, 1)?;
428    state.insert(1)?;
429    // table_set_i pops the top; args shift from n+1..=2 down to 1..=n as we pop
430    for i in (1..=n).rev() {
431        state.table_set_i(1, i as i64)?;
432    }
433    state.push(LuaValue::Int(n as i64));
434    // TODO(port): state.set_field(stack_pos, key_bytes) → lua_setfield; verify method name
435    state.set_field(1, b"n")?;
436    Ok(1)
437}
438
439///
440/// Pushes `t[i], t[i+1], …, t[j]` and returns the count.
441///
442/// ```c
443/// static int tunpack (lua_State *L) {
444///   lua_Unsigned n;
445///   lua_Integer i = luaL_optinteger(L, 2, 1);
446///   lua_Integer e = luaL_opt(L, luaL_checkinteger, 3, luaL_len(L, 1));
447///   if (i > e) return 0;
448///   n = (lua_Unsigned)e - i;
449///   if (l_unlikely(n >= (unsigned int)INT_MAX ||
450///                  !lua_checkstack(L, (int)(++n))))
451///     return luaL_error(L, "too many results to unpack");
452///   for (; i < e; i++) lua_geti(L, 1, i);
453///   lua_geti(L, 1, e);
454///   return (int)n;
455/// }
456/// ```
457pub fn unpack(state: &mut LuaState) -> Result<usize, LuaError> {
458    let i = state.opt_arg_integer(2, 1)?;
459    let e = if matches!(state.type_at(3), LuaType::None | LuaType::Nil) {
460        state.length_at(1)?
461    } else {
462        state.check_arg_integer(3)?
463    };
464    if i > e {
465        return Ok(0); // empty range
466    }
467    let n = (e as u64).wrapping_sub(i as u64);
468    // The size check uses the pre-increment value so that a wrapped-to-0 result
469    // (e.g. i=minI, e=maxI yields n = 2^64-1 pre-inc, 0 post-inc) still trips
470    // the error rather than silently entering a 2^64-iteration loop.
471    if n >= i32::MAX as u64 {
472        return Err(LuaError::runtime(format_args!("too many results to unpack")));
473    }
474    let n = n + 1;
475    if !state.check_stack_growth(n as i32) {
476        return Err(LuaError::runtime(format_args!("too many results to unpack")));
477    }
478    let n = n as i64;
479    let mut k = i;
480    while k < e {
481        state.table_get_i(1, k)?;
482        k += 1;
483    }
484    state.table_get_i(1, e)?; // push last element
485    Ok(n as usize)
486}
487
488// ─── Quicksort ────────────────────────────────────────────────────────────────
489
490/// selection when a partition is severely imbalanced.
491///
492/// `unsigned int` array whose elements are summed.
493///
494/// PORT NOTE: C uses a small randomised pivot guard to avoid pathological sort
495/// partitions. The Rust port asks the host for entropy when available and falls
496/// back to a deterministic pivot value in sandboxed/bare-WASM hosts.
497fn randomize_pivot(state: &LuaState) -> u32 {
498    let entropy = state.global().entropy_hook.map(|hook| hook()).unwrap_or(0);
499    let mixed = entropy ^ entropy.wrapping_shr(32);
500    (mixed as u32) ^ (mixed as u32).wrapping_shr(16)
501}
502
503/// `table[i]` and `table[j]` respectively (table is at stack position 1).
504///
505/// ```c
506/// static void set2 (lua_State *L, IdxT i, IdxT j) {
507///   lua_seti(L, 1, i);
508///   lua_seti(L, 1, j);
509/// }
510/// ```
511fn set2(state: &mut LuaState, i: IdxT, j: IdxT) -> Result<(), LuaError> {
512    // First seti pops the stack top; second seti pops the new top.
513    state.table_set_i(1, i as i64)?;
514    state.table_set_i(1, j as i64)?;
515    Ok(())
516}
517
518/// sort order: either the `<` operator (if arg 2 is nil) or the user's
519/// comparison function at stack position 2.
520///
521/// ```c
522/// static int sort_comp (lua_State *L, int a, int b) {
523///   if (lua_isnil(L, 2))
524///     return lua_compare(L, a, b, LUA_OPLT);
525///   else {
526///     int res;
527///     lua_pushvalue(L, 2);
528///     lua_pushvalue(L, a-1);
529///     lua_pushvalue(L, b-2);
530///     lua_call(L, 2, 1);
531///     res = lua_toboolean(L, -1);
532///     lua_pop(L, 1);
533///     return res;
534///   }
535/// }
536/// ```
537///
538/// The offsets `a-1` and `b-2` compensate for the function and first-argument
539/// copies pushed before the respective values: `a-1` accounts for the function
540/// push; `b-2` accounts for both the function push and the copy of `a`.
541fn sort_comp(state: &mut LuaState, a: i32, b: i32) -> Result<bool, LuaError> {
542    if state.type_at(2) == LuaType::Nil {
543        // No user comparator: use the default `<` operator.
544        return state.compare(a, b, CompareOp::Lt);
545    }
546    // User comparator at stack position 2.
547    state.push_value_at(2)?;       // push function
548    state.push_value_at(a - 1)?;   // push copy of a (compensate for function push)
549    state.push_value_at(b - 2)?;   // push copy of b (compensate for function + a copy)
550    state.call(2, 1)?;
551    // TODO(port): state.to_boolean(-1) → lua_toboolean (never fails); verify method name
552    let res = state.to_boolean(-1);
553    state.pop_n(1);
554    Ok(res)
555}
556
557/// is already on the top of the Lua stack.
558///
559/// Precondition: `a[lo] <= P == a[up-1] <= a[up]` and `P` is at stack top.
560/// Postcondition: `a[lo..i-1] <= a[i] == P <= a[i+1..up]`; stack is clean.
561/// Returns the final pivot index `i`.
562///
563/// ```c
564/// static IdxT partition (lua_State *L, IdxT lo, IdxT up) {
565///   IdxT i = lo;
566///   IdxT j = up - 1;
567///   for (;;) {
568///     while ((void)lua_geti(L, 1, ++i), sort_comp(L, -1, -2)) {
569///       if (l_unlikely(i == up - 1))
570///         luaL_error(L, "invalid order function for sorting");
571///       lua_pop(L, 1);
572///     }
573///     while ((void)lua_geti(L, 1, --j), sort_comp(L, -3, -1)) {
574///       if (l_unlikely(j < i))
575///         luaL_error(L, "invalid order function for sorting");
576///       lua_pop(L, 1);
577///     }
578///     if (j < i) {
579///       lua_pop(L, 1);
580///       set2(L, up - 1, i);
581///       return i;
582///     }
583///     set2(L, i, j);
584///   }
585/// }
586/// ```
587fn partition(state: &mut LuaState, lo: IdxT, up: IdxT) -> Result<IdxT, LuaError> {
588    let mut i: IdxT = lo;
589    let mut j: IdxT = up - 1;
590    // Entry: stack top is P (pivot value).
591    loop {
592        // Advance i: find first a[i] >= P.
593        // Stack during i-loop body: P(-2), a[i](-1)
594        loop {
595            i += 1;
596            state.table_get_i(1, i as i64)?; // push a[i]
597            if !sort_comp(state, -1, -2)? {
598                // a[i] >= P: leave a[i] on stack and exit
599                break;
600            }
601            // a[i] < P; check for invalid comparator
602            if i == up - 1 {
603                return Err(LuaError::runtime(format_args!(
604                    "invalid order function for sorting"
605                )));
606            }
607            state.pop_n(1); // remove a[i]
608        }
609        // Retreat j: find last a[j] <= P.
610        // Stack during j-loop body: P(-3), a[i](-2), a[j](-1)
611        loop {
612            // PERF(port): wrapping_sub mirrors C unsigned IdxT behaviour for edge cases
613            j = j.wrapping_sub(1);
614            state.table_get_i(1, j as i64)?; // push a[j]
615            if !sort_comp(state, -3, -1)? {
616                // P >= a[j]: leave a[j] on stack and exit
617                break;
618            }
619            // P < a[j]; check for invalid comparator
620            if j < i {
621                return Err(LuaError::runtime(format_args!(
622                    "invalid order function for sorting"
623                )));
624            }
625            state.pop_n(1); // remove a[j]
626        }
627        // Stack: P(-3), a[i](-2), a[j](-1)
628        if j < i {
629            // No out-of-place elements; finalize: place pivot at position i.
630            state.pop_n(1); // pop a[j]; stack: P(-2), a[i](-1)
631            set2(state, up - 1, i)?; // table[up-1] = a[i], table[i] = P; stack clean
632            return Ok(i);
633        }
634        // Swap a[i] and a[j] to restore loop invariant.
635        // set2: table[i] = a[j] (pops -1), table[j] = a[i] (pops new -1); stack: P(-1)
636        set2(state, i, j)?;
637    }
638}
639
640/// `[lo, up]`, randomised by `rnd`.
641///
642/// ```c
643/// static IdxT choosePivot (IdxT lo, IdxT up, unsigned int rnd) {
644///   IdxT r4 = (up - lo) / 4;
645///   IdxT p = rnd % (r4 * 2) + (lo + r4);
646///   lua_assert(lo + r4 <= p && p <= up - r4);
647///   return p;
648/// }
649/// ```
650fn choose_pivot(lo: IdxT, up: IdxT, rnd: u32) -> IdxT {
651    let r4 = (up - lo) / 4; // range / 4
652    let p = rnd % (r4 * 2) + (lo + r4);
653    debug_assert!(lo + r4 <= p && p <= up - r4);
654    p
655}
656
657///
658/// Sorts `table[lo..=up]` in place, recursing on the smaller partition and
659/// tail-looping on the larger (to bound Rust's call stack). Randomises pivot
660/// selection when a partition is badly imbalanced.
661///
662/// ```c
663/// static void auxsort (lua_State *L, IdxT lo, IdxT up, unsigned int rnd) {
664///   while (lo < up) {
665///     IdxT p, n;
666///     lua_geti(L, 1, lo); lua_geti(L, 1, up);
667///     if (sort_comp(L, -1, -2)) set2(L, lo, up); else lua_pop(L, 2);
668///     if (up - lo == 1) return;
669///     if (up - lo < RANLIMIT || rnd == 0) p = (lo + up)/2;
670///     else p = choosePivot(lo, up, rnd);
671///     lua_geti(L, 1, p); lua_geti(L, 1, lo);
672///     if (sort_comp(L, -2, -1)) set2(L, p, lo);
673///     else {
674///       lua_pop(L, 1); lua_geti(L, 1, up);
675///       if (sort_comp(L, -1, -2)) set2(L, p, up); else lua_pop(L, 2);
676///     }
677///     if (up - lo == 2) return;
678///     lua_geti(L, 1, p); lua_pushvalue(L, -1); lua_geti(L, 1, up - 1);
679///     set2(L, p, up - 1);
680///     p = partition(L, lo, up);
681///     if (p - lo < up - p) {
682///       auxsort(L, lo, p - 1, rnd); n = p - lo; lo = p + 1;
683///     } else {
684///       auxsort(L, p + 1, up, rnd); n = up - p; up = p - 1;
685///     }
686///     if ((up - lo) / 128 > n) rnd = l_randomizePivot();
687///   }
688/// }
689/// ```
690fn aux_sort(state: &mut LuaState, mut lo: IdxT, mut up: IdxT, mut rnd: u32) -> Result<(), LuaError> {
691    while lo < up {
692        // Step 1: ensure a[lo] <= a[up] (cheap two-element sort)
693        state.table_get_i(1, lo as i64)?; // push a[lo]
694        state.table_get_i(1, up as i64)?; // push a[up]
695        if sort_comp(state, -1, -2)? {
696            set2(state, lo, up)?; // swap so a[lo] <= a[up]
697        } else {
698            state.pop_n(2);
699        }
700        if up - lo == 1 {
701            return Ok(()); // only 2 elements, now sorted
702        }
703
704        // Step 2: choose pivot index
705        let mut p: IdxT = if up - lo < RANLIMIT || rnd == 0 {
706            (lo + up) / 2 // midpoint pivot for small/non-random runs
707        } else {
708            choose_pivot(lo, up, rnd)
709        };
710
711        // Step 3: median-of-three: sort a[lo], a[p], a[up]
712        state.table_get_i(1, p as i64)?;  // push a[p]
713        state.table_get_i(1, lo as i64)?; // push a[lo]
714        if sort_comp(state, -2, -1)? {
715            set2(state, p, lo)?; // swap a[p] ↔ a[lo]; stack clean
716        } else {
717            state.pop_n(1); // remove a[lo]; stack: a[p]
718            state.table_get_i(1, up as i64)?; // push a[up]; stack: a[p], a[up]
719            if sort_comp(state, -1, -2)? {
720                set2(state, p, up)?; // swap a[p] ↔ a[up]; stack clean
721            } else {
722                state.pop_n(2); // remove a[up] and a[p]; stack clean
723            }
724        }
725        // Stack is clean at this point.
726        if up - lo == 2 {
727            return Ok(()); // only 3 elements, now sorted
728        }
729
730        // Step 4: move pivot to a[up-1] and call partition.
731        //
732        // Stack evolution:
733        //   table_get_i(p):    a[p]  (-1)
734        //   push_value_at(-1): a[p]  (-2), a[p]_copy  (-1)
735        //   table_get_i(up-1): a[p]  (-3), a[p]_copy  (-2), a[up-1]  (-1)
736        //   set2(p, up-1):     table[p] = a[up-1], table[up-1] = a[p]_copy;
737        //                      stack: a[p] (-1)  ← pivot for partition
738        state.table_get_i(1, p as i64)?;
739        state.push_value_at(-1)?; // duplicate: two copies of pivot on stack
740        state.table_get_i(1, (up - 1) as i64)?;
741        set2(state, p, up - 1)?;
742        // One copy of the pivot value remains at the stack top for partition.
743
744        p = partition(state, lo, up)?;
745        // Stack is clean after partition returns.
746
747        // Step 5: recurse on smaller partition; tail-loop on larger.
748        let n: IdxT;
749        if p - lo < up - p {
750            aux_sort(state, lo, p - 1, rnd)?;
751            n = p - lo;
752            lo = p + 1; // tail: sort [p+1 .. up]
753        } else {
754            aux_sort(state, p + 1, up, rnd)?;
755            n = up - p;
756            up = p - 1; // tail: sort [lo .. p-1]
757        }
758
759        // Re-randomise if the partition was severely imbalanced.
760        if (up - lo) / 128 > n {
761            rnd = randomize_pivot(state);
762        }
763    }
764    Ok(())
765}
766
767///
768/// ```c
769/// static int sort (lua_State *L) {
770///   lua_Integer n = aux_getn(L, 1, TAB_RW);
771///   if (n > 1) {
772///     luaL_argcheck(L, n < INT_MAX, 1, "array too big");
773///     if (!lua_isnoneornil(L, 2))
774///       luaL_checktype(L, 2, LUA_TFUNCTION);
775///     lua_settop(L, 2);
776///     auxsort(L, 1, (IdxT)n, 0);
777///   }
778///   return 0;
779/// }
780/// ```
781pub fn sort(state: &mut LuaState) -> Result<usize, LuaError> {
782    let n = aux_getn(state, 1, TAB_RW)?;
783    if n > 1 {
784        if !(n < i32::MAX as i64) {
785            return Err(lua_vm::debug::arg_error_impl(state, 1, b"array too big"));
786        }
787        if !matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
788            state.check_arg_type(2, LuaType::Function)?;
789        }
790        // Must go through the public C-API set_top (relative to the call
791        // frame); the inherent LuaState::set_top treats its argument as
792        // an absolute stack slot and would corrupt the frame.
793        lua_vm::api::set_top(state, 2)?;
794        aux_sort(state, 1, n as IdxT, 0)?;
795    }
796    Ok(0)
797}
798
799// ─── Registration ─────────────────────────────────────────────────────────────
800
801///
802/// ```c
803/// static const luaL_Reg tab_funcs[] = {
804///   {"concat", tconcat}, {"insert", tinsert}, {"pack", tpack},
805///   {"unpack", tunpack}, {"remove", tremove}, {"move", tmove},
806///   {"sort", sort}, {NULL, NULL}
807/// };
808/// ```
809///
810/// PORT NOTE: In Rust we represent this as a slice of `(&[u8], fn-ptr)` pairs;
811/// the sentinel `{NULL, NULL}` is implicit (the slice has a known length).
812pub const TABLE_FUNCS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
813    (b"concat", concat),
814    (b"insert", insert),
815    (b"pack", pack),
816    (b"unpack", unpack),
817    (b"remove", remove),
818    (b"move", tmove),
819    (b"sort", sort),
820];
821
822/// `table.create(nseq [, nrec])` — Lua 5.5 addition
823/// (`specs/research/5.5-upstream-delta.md` §5, `ltablib.c`).
824///
825/// Preallocates a table with `nseq` array (sequence) slots and `nrec` hash
826/// (record) slots, returning the empty table. Preallocation is purely a
827/// capacity hint; the returned table is observably empty (length 0, no keys),
828/// so this implementation is behaviorally faithful even though our
829/// `create_table` may treat the sizes as advisory.
830///
831/// Registered into the `table` roster only under [`lua_types::LuaVersion::V55`]
832/// (see [`open_table`]); absent under 5.1-5.4, matching upstream.
833pub fn create(state: &mut LuaState) -> Result<usize, LuaError> {
834    let nseq = state.check_arg_integer(1)?;
835    let nrec = state.opt_arg_integer(2, 0)?;
836    if nseq < 0 || nseq > i32::MAX as i64 {
837        return Err(LuaError::runtime(format_args!(
838            "bad argument #1 to 'create' (size out of range)"
839        )));
840    }
841    if nrec < 0 || nrec > i32::MAX as i64 {
842        return Err(LuaError::runtime(format_args!(
843            "bad argument #2 to 'create' (size out of range)"
844        )));
845    }
846    state.create_table(nseq as i32, nrec as i32)?;
847    Ok(1)
848}
849
850// ─── Lua 5.1 legacy compat functions (`getn`/`setn`/`maxn`/`foreach`/`foreachi`) ──
851//
852// These predate the `#` operator and the 5.2 roster cleanup; they ship only in
853// the default lua5.1.5 build (`ltablib.c`) and are registered under the V51
854// backend by `open_table`. Verified against lua5.1.5; see
855// specs/followup/5.1-roster-syntax.md §1.
856
857/// `table.getn(t)` — the "size" of a sequence, i.e. the border `#t` reports.
858///
859/// In 5.1 `aux_getn` is `luaL_checktype(TABLE)` followed by `luaL_getn`, which
860/// resolves to the primitive length. Mirrors `getn` in 5.1 `ltablib.c`.
861fn getn(state: &mut LuaState) -> Result<usize, LuaError> {
862    state.check_arg_type(1, LuaType::Table)?;
863    let n = state.length_at(1)?;
864    state.push(LuaValue::Int(n));
865    Ok(1)
866}
867
868/// `table.setn(t, n)` — obsolete gravestone. In 5.1 the default build defines
869/// `luaL_setn` as a no-op, so `setn` raises `'setn' is obsolete`. Verified
870/// against lua5.1.5 (`pcall`-able to that exact message).
871fn setn(state: &mut LuaState) -> Result<usize, LuaError> {
872    state.check_arg_type(1, LuaType::Table)?;
873    Err(LuaError::runtime(format_args!("'setn' is obsolete")))
874}
875
876/// `table.maxn(t)` — the largest positive numeric key (0 if none). Iterates the
877/// raw table via `next`, tracking the max numeric key. Mirrors `maxn` in 5.1
878/// `ltablib.c`.
879fn maxn(state: &mut LuaState) -> Result<usize, LuaError> {
880    state.check_arg_type(1, LuaType::Table)?;
881    let mut max: f64 = 0.0;
882    state.push(LuaValue::Nil);
883    while state.table_next(1)? {
884        // Stack: ..., key, value. Drop the value, inspect the key.
885        state.pop_n(1);
886        if matches!(state.type_at(-1), LuaType::Number) {
887            if let Some(v) = state.to_number(-1) {
888                if v > max {
889                    max = v;
890                }
891            }
892        }
893    }
894    state.push(LuaValue::Float(max));
895    Ok(1)
896}
897
898/// `table.foreachi(t, f)` — call `f(i, t[i])` for `i` in `1..#t`, stopping early
899/// if `f` returns a non-nil value (which is then returned). Mirrors `foreachi`
900/// in 5.1 `ltablib.c`.
901fn foreachi(state: &mut LuaState) -> Result<usize, LuaError> {
902    state.check_arg_type(1, LuaType::Table)?;
903    state.check_arg_type(2, LuaType::Function)?;
904    let n = state.length_at(1)?;
905    let mut i: i64 = 1;
906    while i <= n {
907        state.push_value_at(2)?;
908        state.push(LuaValue::Int(i));
909        state.table_get_i(1, i)?;
910        state.call(2, 1)?;
911        if !matches!(state.type_at(-1), LuaType::Nil) {
912            return Ok(1);
913        }
914        state.pop_n(1);
915        i += 1;
916    }
917    Ok(0)
918}
919
920/// `table.foreach(t, f)` — call `f(k, v)` for every pair, stopping early if `f`
921/// returns a non-nil value (which is then returned). Mirrors `foreach` in 5.1
922/// `ltablib.c`.
923fn foreach(state: &mut LuaState) -> Result<usize, LuaError> {
924    state.check_arg_type(1, LuaType::Table)?;
925    state.check_arg_type(2, LuaType::Function)?;
926    state.push(LuaValue::Nil);
927    while state.table_next(1)? {
928        // Stack: ..., key, value.
929        state.push_value_at(2)?; // function
930        state.push_value_at(-3)?; // key copy
931        state.push_value_at(-3)?; // value copy
932        state.call(2, 1)?;
933        if !matches!(state.type_at(-1), LuaType::Nil) {
934            return Ok(1);
935        }
936        state.pop_n(2); // remove value and result, leaving key for next()
937    }
938    Ok(0)
939}
940
941// ─── Module opener ────────────────────────────────────────────────────────────
942
943///
944/// ```c
945/// LUAMOD_API int luaopen_table (lua_State *L) {
946///   luaL_newlib(L, tab_funcs);
947///   return 1;
948/// }
949/// ```
950pub fn open_table(state: &mut LuaState) -> Result<usize, LuaError> {
951    // TODO(port): state.new_lib → luaL_newlib; creates a new table and registers functions;
952    //             verify method name and signature
953    //
954    // Per-version roster deltas:
955    //  - `table.move` is a Lua 5.3 addition, absent in 5.1/5.2 (verified against
956    //    lua5.2.4: `type(table.move)` == "nil").
957    //  - `table.pack`/`table.unpack` are Lua 5.2 additions; in 5.1 `unpack` is a
958    //    *global* and there is no `table.pack` (verified against lua5.1.5: both
959    //    `table.unpack` and `table.pack` are nil). 5.1 instead carries the legacy
960    //    `getn`/`setn`/`maxn`/`foreach`/`foreachi` roster.
961    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
962        let legacy: Vec<(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)> = TABLE_FUNCS
963            .iter()
964            .filter(|(name, _)| {
965                *name != b"move".as_slice()
966                    && *name != b"pack".as_slice()
967                    && *name != b"unpack".as_slice()
968            })
969            .copied()
970            .collect();
971        state.new_lib(&legacy)?;
972        const LEGACY_FUNCS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
973            (b"getn", getn),
974            (b"setn", setn),
975            (b"maxn", maxn),
976            (b"foreach", foreach),
977            (b"foreachi", foreachi),
978        ];
979        state.set_funcs_with_upvalues(LEGACY_FUNCS, 0)?;
980    } else if matches!(state.global().lua_version, lua_types::LuaVersion::V52) {
981        let without_move: Vec<(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)> = TABLE_FUNCS
982            .iter()
983            .filter(|(name, _)| *name != b"move".as_slice())
984            .copied()
985            .collect();
986        state.new_lib(&without_move)?;
987    } else {
988        state.new_lib(TABLE_FUNCS)?;
989    }
990    // Per-version roster delta: `table.create` is a Lua 5.5 addition
991    // (`specs/research/5.5-upstream-delta.md` §5), absent in 5.1-5.4. Register
992    // it only on the V55 backend so the version seam carries a real,
993    // script-observable stdlib difference. `new_lib` leaves the new table on
994    // the stack top, so we register `create` into it directly.
995    if matches!(state.global().lua_version, lua_types::LuaVersion::V55) {
996        const CREATE_FUNCS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] =
997            &[(b"create", create)];
998        state.set_funcs_with_upvalues(CREATE_FUNCS, 0)?;
999    }
1000    Ok(1)
1001}
1002
1003// ──────────────────────────────────────────────────────────────────────────────
1004// PORT STATUS
1005//   source:        src/ltablib.c  (430 lines, 14 functions)
1006//   target_crate:  lua-stdlib
1007//   confidence:    medium
1008//   todos:         17
1009//   port_notes:    5
1010//   unsafe_blocks: 0
1011//   notes:         Logic is faithfully translated. All TODOs are method-name
1012//                  uncertainties for LuaState API calls (table_get_i, table_set_i,
1013//                  opt_arg_integer, opt_arg_lstring, get_metatable, to_bytes_at,
1014//                  push_lstring, push_value_at, compare, new_lib, set_top,
1015//                  check_stack_growth, type_name_str_at, create_table) — Phase B
1016//                  maps these to the real method names once lua-vm is drafted.
1017//                  Stack cleanup on error paths is elided (C uses longjmp);
1018//                  needs Phase B attention in check_tab and add_field.
1019//                  PERF: remove() and insert() shift loops now cache the table
1020//                  value once (via value_at) and call table_get_i_value /
1021//                  table_set_i_value, bypassing the per-iteration index_to_value
1022//                  call. This shrank the index_to_value hot frame and improved
1023//                  table_ops_long from ~4.76x to ~4.02x vs reference.
1024// ──────────────────────────────────────────────────────────────────────────────