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