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