lua_stdlib/table_lib.rs
1//! 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`.
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**.
185pub fn remove(state: &mut LuaState) -> Result<usize, LuaError> {
186 let size = check_table_and_get_len(state, 1, TAB_RW)?;
187 let mut pos = state.opt_arg_integer(2, size)?;
188 if state.global().lua_version == lua_types::LuaVersion::V51 {
189 if !(1 <= pos && pos <= size) {
190 return Ok(0);
191 }
192 } else if pos != size {
193 if !((pos as u64).wrapping_sub(1) <= (size as u64)) {
194 let argn = match state.global().lua_version {
195 lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53 => 1,
196 _ => 2,
197 };
198 return Err(lua_vm::debug::arg_error_impl(
199 state,
200 argn,
201 b"position out of bounds",
202 ));
203 }
204 }
205 // Cache the table once to avoid re-resolving stack slot 1 on every
206 // iteration of the shift loop. C's lua_geti is a single pointer
207 // arithmetic operation; our index_to_value is a function call with
208 // branches, so this saves ~2N index resolutions for shift count N.
209 if let Some(tbl) = plain_table_at(state, 1) {
210 let result = tbl.get_int(pos);
211 state.push(result);
212 while pos < size {
213 let shifted = tbl.get_int(pos + 1);
214 raw_set_int(state, tbl, pos, shifted)?;
215 pos += 1;
216 }
217 raw_set_int(state, tbl, pos, LuaValue::Nil)?;
218 return Ok(1);
219 }
220 let tbl = state.value_at(1);
221 state.table_get_i_value(&tbl, pos)?; // push element to be returned
222 while pos < size {
223 state.table_get_i_value(&tbl, pos + 1)?;
224 state.table_set_i_value(&tbl, pos)?;
225 pos += 1;
226 }
227 state.push(LuaValue::Nil);
228 state.table_set_i_value(&tbl, pos)?; // remove last slot (table[pos] = nil)
229 Ok(1)
230}
231
232// ─── table.move ───────────────────────────────────────────────────────────────
233
234/// `table.move(a1, f, e, t, [a2])`. Copies `a1[f..e]` into `a2[t..]` (or
235/// `a1[t..]` if `a2` is absent), reading source slots through `__index` and
236/// writing destinations through `__newindex` one element at a time. To survive
237/// an overlapping in-place range, it copies FORWARD (increasing index) when the
238/// destination is clear of the source's tail (`t > e || t <= f`, or a distinct
239/// destination table) and BACKWARD (decreasing) otherwise — the order pinned by
240/// `v53_plus_move_*`. Returns the destination table. A 5.3 addition.
241pub fn tmove(state: &mut LuaState) -> Result<usize, LuaError> {
242 let f = state.check_arg_integer(2)?;
243 let e = state.check_arg_integer(3)?;
244 let t = state.check_arg_integer(4)?;
245 let tt: i32 = if !matches!(state.type_at(5), LuaType::None | LuaType::Nil) {
246 5
247 } else {
248 1
249 };
250 check_tab(state, 1, TAB_R)?;
251 check_tab(state, tt, TAB_W)?;
252
253 if e >= f {
254 if !(f > 0 || e < i64::MAX + f) {
255 return Err(lua_vm::debug::arg_error_impl(
256 state,
257 3,
258 b"too many elements to move",
259 ));
260 }
261 let n = e - f + 1;
262 if !(t <= i64::MAX - n + 1) {
263 return Err(lua_vm::debug::arg_error_impl(
264 state,
265 4,
266 b"destination wrap around",
267 ));
268 }
269 // Copy forward (increasing) when safe to do so; backward when ranges overlap.
270 let copy_forward = t > e || t <= f || (tt != 1 && !state.compare(1, tt, CompareOp::Eq)?);
271 if copy_forward {
272 for i in 0..n {
273 state.table_get_i(1, f + i)?;
274 state.table_set_i(tt, t + i)?;
275 }
276 } else {
277 for i in (0..n).rev() {
278 state.table_get_i(1, f + i)?;
279 state.table_set_i(tt, t + i)?;
280 }
281 }
282 }
283 state.push_value_at(tt)?;
284 Ok(1)
285}
286
287// ─── table.concat ─────────────────────────────────────────────────────────────
288
289/// Fetches `t[idx]`; if it is a string-or-number, appends its string form to
290/// `buf` and pops it. A non-coercible element raises the exact "invalid value
291/// (<type>) at index <idx> in table for 'concat'" message (pinned by
292/// `v_table_concat_invalid_value_type_name`). The accumulator is a borrowed
293/// `Vec<u8>` rather than C's stack-backed `luaL_Buffer`.
294fn add_field(state: &mut LuaState, buf: &mut Vec<u8>, idx: i64) -> Result<(), LuaError> {
295 state.table_get_i(1, idx)?;
296 if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
297 let type_name = state.type_name_str_at(-1);
298 let msg = format!(
299 "invalid value ({}) at index {} in table for 'concat'",
300 String::from_utf8_lossy(type_name),
301 idx
302 );
303 return crate::auxlib::lua_error(state, msg.as_bytes()).map(|_| ());
304 }
305 let bytes = state
306 .to_bytes_at(-1)
307 .ok_or_else(|| LuaError::runtime(format_args!("invalid value at index {}", idx)))?;
308 buf.extend_from_slice(&bytes);
309 state.pop_n(1);
310 Ok(())
311}
312
313/// `table.concat(t, [sep, [i, [j]]])`. Joins `t[i..j]` (defaults `i=1`,
314/// `j=#t`) with `sep` (default empty) into one string. Each element must be a
315/// string or number; the first that is not raises through [`add_field`].
316pub fn concat(state: &mut LuaState) -> Result<usize, LuaError> {
317 let last = check_table_and_get_len(state, 1, TAB_R)?;
318 // Clone the separator before any stack-mutating calls that might invalidate it.
319 let sep: Vec<u8> = state.opt_arg_lstring(2, Some(b""))?.unwrap_or_default();
320 let mut i = state.opt_arg_integer(3, 1)?;
321 let last = state.opt_arg_integer(4, last)?;
322
323 // A borrowed Vec<u8> accumulates the result, pushed once at the end —
324 // rather than C's luaL_Buffer, which can back-patch the live Lua stack.
325 let mut buf: Vec<u8> = Vec::new();
326 while i < last {
327 add_field(state, &mut buf, i)?;
328 buf.extend_from_slice(&sep);
329 i += 1;
330 }
331 if i == last {
332 add_field(state, &mut buf, i)?;
333 }
334 state.push_lstring(&buf)?;
335 Ok(1)
336}
337
338// ─── table.pack / table.unpack ────────────────────────────────────────────────
339
340/// `table.pack(...)`. Creates a new table with the arguments at integer keys
341/// `1..n` and `t.n` set to the *literal* argument count `n` — holes and
342/// trailing nils included, so `.n` recovers an arity that a `#t` border would
343/// lose (pinned by `v52_plus_pack_n_field_*`). A 5.2 addition.
344pub fn pack(state: &mut LuaState) -> Result<usize, LuaError> {
345 let n = state.get_top();
346 state.create_table(n, 1)?;
347 state.insert(1)?;
348 // table_set_i pops the top; args shift from n+1..=2 down to 1..=n as we pop
349 for i in (1..=n).rev() {
350 state.table_set_i(1, i as i64)?;
351 }
352 state.push(LuaValue::Int(n as i64));
353 state.set_field(1, b"n")?;
354 Ok(1)
355}
356
357/// `table.unpack(t, [i, [j]])`. Pushes `t[i], t[i+1], …, t[j]` (defaults
358/// `i=1`, `j=#t`) and returns the count. An `i > e` range is empty (zero
359/// results); a span of `INT_MAX` or more — including the i64-extreme wrap where
360/// `e - i` overflows to a huge unsigned value — raises "too many results to
361/// unpack" rather than attempting the push (pinned by
362/// `v52_plus_unpack_*` / `v53_plus_unpack_*`). A 5.2 addition.
363pub fn unpack(state: &mut LuaState) -> Result<usize, LuaError> {
364 let i = state.opt_arg_integer(2, 1)?;
365 let e = if matches!(state.type_at(3), LuaType::None | LuaType::Nil) {
366 state.length_at(1)?
367 } else {
368 state.check_arg_integer(3)?
369 };
370 if i > e {
371 return Ok(0); // empty range
372 }
373 let n = (e as u64).wrapping_sub(i as u64);
374 // The size check uses the pre-increment value so that a wrapped-to-0 result
375 // (e.g. i=minI, e=maxI yields n = 2^64-1 pre-inc, 0 post-inc) still trips
376 // the error rather than silently entering a 2^64-iteration loop.
377 if n >= i32::MAX as u64 {
378 return Err(LuaError::runtime(format_args!(
379 "too many results to unpack"
380 )));
381 }
382 let n = n + 1;
383 if !state.check_stack_growth(n as i32) {
384 return Err(LuaError::runtime(format_args!(
385 "too many results to unpack"
386 )));
387 }
388 let n = n as i64;
389 let mut k = i;
390 while k < e {
391 state.table_get_i(1, k)?;
392 k += 1;
393 }
394 state.table_get_i(1, e)?; // push last element
395 Ok(n as usize)
396}
397
398// ─── Quicksort ────────────────────────────────────────────────────────────────
399
400/// A small pseudo-random value used to bias quicksort's pivot selection when
401/// a partition is severely imbalanced.
402///
403/// C uses a small randomised pivot guard to avoid pathological sort
404/// partitions. The Rust port asks the host for entropy when available and
405/// falls back to a deterministic pivot value in sandboxed/bare-WASM hosts.
406fn randomize_pivot(state: &LuaState) -> u32 {
407 let entropy = state.global().entropy_hook.map(|hook| hook()).unwrap_or(0);
408 let mixed = entropy ^ entropy.wrapping_shr(32);
409 (mixed as u32) ^ (mixed as u32).wrapping_shr(16)
410}
411
412/// Sets `table[i]` and `table[j]` respectively (table is at stack position 1).
413fn set2(state: &mut LuaState, i: IdxT, j: IdxT) -> Result<(), LuaError> {
414 // First seti pops the stack top; second seti pops the new top.
415 state.table_set_i(1, i as i64)?;
416 state.table_set_i(1, j as i64)?;
417 Ok(())
418}
419
420/// Compares elements `a` and `b` using the current sort order: either the
421/// `<` operator (if arg 2 is nil) or the user's comparison function at
422/// stack position 2.
423///
424/// The offsets `a-1` and `b-2` compensate for the function and first-argument
425/// copies pushed before the respective values: `a-1` accounts for the function
426/// push; `b-2` accounts for both the function push and the copy of `a`.
427fn sort_comp(state: &mut LuaState, a: i32, b: i32) -> Result<bool, LuaError> {
428 if state.type_at(2) == LuaType::Nil {
429 // No user comparator: use the default `<` operator.
430 return state.compare(a, b, CompareOp::Lt);
431 }
432 // User comparator at stack position 2.
433 state.push_value_at(2)?; // push function
434 state.push_value_at(a - 1)?; // push copy of a (compensate for function push)
435 state.push_value_at(b - 2)?; // push copy of b (compensate for function + a copy)
436 state.call(2, 1)?;
437 let res = state.to_boolean(-1);
438 state.pop_n(1);
439 Ok(res)
440}
441
442/// Partitions `table[lo..=up]` around the pivot value, which the caller has
443/// already pushed onto the top of the Lua stack.
444///
445/// Precondition: `a[lo] <= P == a[up-1] <= a[up]` and `P` is at stack top.
446/// Postcondition: `a[lo..i-1] <= a[i] == P <= a[i+1..up]`; stack is clean.
447/// Returns the final pivot index `i`.
448fn partition(state: &mut LuaState, lo: IdxT, up: IdxT) -> Result<IdxT, LuaError> {
449 let mut i: IdxT = lo;
450 let mut j: IdxT = up - 1;
451 // Entry: stack top is P (pivot value).
452 loop {
453 // Advance i: find first a[i] >= P.
454 // Stack during i-loop body: P(-2), a[i](-1)
455 loop {
456 i += 1;
457 state.table_get_i(1, i as i64)?; // push a[i]
458 if !sort_comp(state, -1, -2)? {
459 // a[i] >= P: leave a[i] on stack and exit
460 break;
461 }
462 // a[i] < P; check for invalid comparator
463 if i == up - 1 {
464 return Err(LuaError::runtime(format_args!(
465 "invalid order function for sorting"
466 )));
467 }
468 state.pop_n(1); // remove a[i]
469 }
470 // Retreat j: find last a[j] <= P.
471 // Stack during j-loop body: P(-3), a[i](-2), a[j](-1)
472 loop {
473 // wrapping_sub mirrors C's unsigned IdxT behavior for edge cases.
474 j = j.wrapping_sub(1);
475 state.table_get_i(1, j as i64)?; // push a[j]
476 if !sort_comp(state, -3, -1)? {
477 // P >= a[j]: leave a[j] on stack and exit
478 break;
479 }
480 // P < a[j]; check for invalid comparator
481 if j < i {
482 return Err(LuaError::runtime(format_args!(
483 "invalid order function for sorting"
484 )));
485 }
486 state.pop_n(1); // remove a[j]
487 }
488 // Stack: P(-3), a[i](-2), a[j](-1)
489 if j < i {
490 // No out-of-place elements; finalize: place pivot at position i.
491 state.pop_n(1); // pop a[j]; stack: P(-2), a[i](-1)
492 set2(state, up - 1, i)?; // table[up-1] = a[i], table[i] = P; stack clean
493 return Ok(i);
494 }
495 // Swap a[i] and a[j] to restore loop invariant.
496 // set2: table[i] = a[j] (pops -1), table[j] = a[i] (pops new -1); stack: P(-1)
497 set2(state, i, j)?;
498 }
499}
500
501/// Chooses a pivot index within `[lo, up]`, randomised by `rnd`.
502fn choose_pivot(lo: IdxT, up: IdxT, rnd: u32) -> IdxT {
503 let r4 = (up - lo) / 4; // range / 4
504 let p = rnd % (r4 * 2) + (lo + r4);
505 debug_assert!(lo + r4 <= p && p <= up - r4);
506 p
507}
508
509/// Sorts `table[lo..=up]` in place, recursing on the smaller partition and
510/// tail-looping on the larger (to bound Rust's call stack). Randomises pivot
511/// selection when a partition is badly imbalanced.
512fn aux_sort(
513 state: &mut LuaState,
514 mut lo: IdxT,
515 mut up: IdxT,
516 mut rnd: u32,
517) -> Result<(), LuaError> {
518 while lo < up {
519 // Step 1: ensure a[lo] <= a[up] (cheap two-element sort)
520 state.table_get_i(1, lo as i64)?; // push a[lo]
521 state.table_get_i(1, up as i64)?; // push a[up]
522 if sort_comp(state, -1, -2)? {
523 set2(state, lo, up)?; // swap so a[lo] <= a[up]
524 } else {
525 state.pop_n(2);
526 }
527 if up - lo == 1 {
528 return Ok(()); // only 2 elements, now sorted
529 }
530
531 // Step 2: choose pivot index
532 let mut p: IdxT = if up - lo < RANLIMIT || rnd == 0 {
533 (lo + up) / 2 // midpoint pivot for small/non-random runs
534 } else {
535 choose_pivot(lo, up, rnd)
536 };
537
538 // Step 3: median-of-three: sort a[lo], a[p], a[up]
539 state.table_get_i(1, p as i64)?; // push a[p]
540 state.table_get_i(1, lo as i64)?; // push a[lo]
541 if sort_comp(state, -2, -1)? {
542 set2(state, p, lo)?; // swap a[p] ↔ a[lo]; stack clean
543 } else {
544 state.pop_n(1); // remove a[lo]; stack: a[p]
545 state.table_get_i(1, up as i64)?; // push a[up]; stack: a[p], a[up]
546 if sort_comp(state, -1, -2)? {
547 set2(state, p, up)?; // swap a[p] ↔ a[up]; stack clean
548 } else {
549 state.pop_n(2); // remove a[up] and a[p]; stack clean
550 }
551 }
552 // Stack is clean at this point.
553 if up - lo == 2 {
554 return Ok(()); // only 3 elements, now sorted
555 }
556
557 // Step 4: move pivot to a[up-1] and call partition.
558 //
559 // Stack evolution:
560 // table_get_i(p): a[p] (-1)
561 // push_value_at(-1): a[p] (-2), a[p]_copy (-1)
562 // table_get_i(up-1): a[p] (-3), a[p]_copy (-2), a[up-1] (-1)
563 // set2(p, up-1): table[p] = a[up-1], table[up-1] = a[p]_copy;
564 // stack: a[p] (-1) ← pivot for partition
565 state.table_get_i(1, p as i64)?;
566 state.push_value_at(-1)?; // duplicate: two copies of pivot on stack
567 state.table_get_i(1, (up - 1) as i64)?;
568 set2(state, p, up - 1)?;
569 // One copy of the pivot value remains at the stack top for partition.
570
571 p = partition(state, lo, up)?;
572 // Stack is clean after partition returns.
573
574 // Step 5: recurse on smaller partition; tail-loop on larger.
575 let n: IdxT;
576 if p - lo < up - p {
577 aux_sort(state, lo, p - 1, rnd)?;
578 n = p - lo;
579 lo = p + 1; // tail: sort [p+1 .. up]
580 } else {
581 aux_sort(state, p + 1, up, rnd)?;
582 n = up - p;
583 up = p - 1; // tail: sort [lo .. p-1]
584 }
585
586 // Re-randomise if the partition was severely imbalanced.
587 if (up - lo) / 128 > n {
588 rnd = randomize_pivot(state);
589 }
590 }
591 Ok(())
592}
593
594/// `table.sort(t, [comp])`: sorts `t` in place using `comp`, or the default
595/// `<` operator if `comp` is absent.
596pub fn sort(state: &mut LuaState) -> Result<usize, LuaError> {
597 let n = check_table_and_get_len(state, 1, TAB_RW)?;
598 if n > 1 {
599 if !(n < i32::MAX as i64) {
600 return Err(lua_vm::debug::arg_error_impl(state, 1, b"array too big"));
601 }
602 if !matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
603 state.check_arg_type(2, LuaType::Function)?;
604 }
605 // Must go through the public C-API set_top (relative to the call
606 // frame); the inherent LuaState::set_top treats its argument as
607 // an absolute stack slot and would corrupt the frame.
608 lua_vm::api::set_top(state, 2)?;
609 aux_sort(state, 1, n as IdxT, 0)?;
610 }
611 Ok(0)
612}
613
614// ─── Registration ─────────────────────────────────────────────────────────────
615
616/// The core `table` roster shared by 5.2-5.5. `move` is filtered out for 5.2
617/// (a 5.3 addition) and `move`/`pack`/`unpack` for 5.1 by [`open_table`], which
618/// also layers the version-specific extras (5.1 legacy, 5.5 `create`) on top.
619pub const TABLE_FUNCS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
620 (b"concat", concat),
621 (b"insert", insert),
622 (b"pack", pack),
623 (b"unpack", unpack),
624 (b"remove", remove),
625 (b"move", tmove),
626 (b"sort", sort),
627];
628
629/// `table.create(nseq [, nrec])` — Lua 5.5 addition
630/// (`specs/research/5.5-upstream-delta.md` §5, `ltablib.c`).
631///
632/// Preallocates a table with `nseq` array (sequence) slots and `nrec` hash
633/// (record) slots, returning the empty table. Preallocation is purely a
634/// capacity hint; the returned table is observably empty (length 0, no keys),
635/// so this implementation is behaviorally faithful even though our
636/// `create_table` may treat the sizes as advisory.
637///
638/// Registered into the `table` roster only under [`lua_types::LuaVersion::V55`]
639/// (see [`open_table`]); absent under 5.1-5.4, matching upstream.
640pub fn create(state: &mut LuaState) -> Result<usize, LuaError> {
641 let nseq = state.check_arg_integer(1)?;
642 let nrec = state.opt_arg_integer(2, 0)?;
643 if nseq < 0 || nseq > i32::MAX as i64 {
644 return Err(LuaError::runtime(format_args!(
645 "bad argument #1 to 'create' (size out of range)"
646 )));
647 }
648 if nrec < 0 || nrec > i32::MAX as i64 {
649 return Err(LuaError::runtime(format_args!(
650 "bad argument #2 to 'create' (size out of range)"
651 )));
652 }
653 state.create_table(nseq as i32, nrec as i32)?;
654 Ok(1)
655}
656
657// ─── Lua 5.1 legacy compat functions (`getn`/`setn`/`maxn`/`foreach`/`foreachi`) ──
658//
659// These predate the `#` operator and the 5.2 roster cleanup; they ship only in
660// the default lua5.1.5 build (`ltablib.c`) and are registered under the V51
661// backend by `open_table`. Verified against lua5.1.5; see
662// specs/followup/5.1-roster-syntax.md §1.
663
664/// `table.getn(t)` — the "size" of a sequence, i.e. the border `#t` reports.
665///
666/// In 5.1 `aux_getn` is `luaL_checktype(TABLE)` followed by `luaL_getn`, which
667/// resolves to the primitive length. Mirrors `getn` in 5.1 `ltablib.c`.
668fn getn(state: &mut LuaState) -> Result<usize, LuaError> {
669 state.check_arg_type(1, LuaType::Table)?;
670 let n = state.length_at(1)?;
671 state.push(LuaValue::Int(n));
672 Ok(1)
673}
674
675/// `table.setn(t, n)` — obsolete gravestone. In 5.1 the default build defines
676/// `luaL_setn` as a no-op, so `setn` raises `'setn' is obsolete`. Verified
677/// against lua5.1.5 (`pcall`-able to that exact message).
678fn setn(state: &mut LuaState) -> Result<usize, LuaError> {
679 state.check_arg_type(1, LuaType::Table)?;
680 Err(LuaError::runtime(format_args!("'setn' is obsolete")))
681}
682
683/// `table.maxn(t)` — the largest positive numeric key (0 if none). Iterates the
684/// raw table via `next`, tracking the max numeric key. Mirrors `maxn` in the 5.1
685/// and 5.2 `ltablib.c`; removed in 5.3. Registered for both V51 and V52 by
686/// [`open_table`].
687fn maxn(state: &mut LuaState) -> Result<usize, LuaError> {
688 state.check_arg_type(1, LuaType::Table)?;
689 let mut max: f64 = 0.0;
690 state.push(LuaValue::Nil);
691 while state.table_next(1)? {
692 // Stack: ..., key, value. Drop the value, inspect the key.
693 state.pop_n(1);
694 if matches!(state.type_at(-1), LuaType::Number) {
695 if let Some(v) = state.to_number(-1) {
696 if v > max {
697 max = v;
698 }
699 }
700 }
701 }
702 state.push(LuaValue::Float(max));
703 Ok(1)
704}
705
706/// `table.foreachi(t, f)` — call `f(i, t[i])` for `i` in `1..#t`, stopping early
707/// if `f` returns a non-nil value (which is then returned). Mirrors `foreachi`
708/// in 5.1 `ltablib.c`.
709fn foreachi(state: &mut LuaState) -> Result<usize, LuaError> {
710 state.check_arg_type(1, LuaType::Table)?;
711 state.check_arg_type(2, LuaType::Function)?;
712 let n = state.length_at(1)?;
713 let mut i: i64 = 1;
714 while i <= n {
715 state.push_value_at(2)?;
716 state.push(LuaValue::Int(i));
717 state.table_get_i(1, i)?;
718 state.call(2, 1)?;
719 if !matches!(state.type_at(-1), LuaType::Nil) {
720 return Ok(1);
721 }
722 state.pop_n(1);
723 i += 1;
724 }
725 Ok(0)
726}
727
728/// `table.foreach(t, f)` — call `f(k, v)` for every pair, stopping early if `f`
729/// returns a non-nil value (which is then returned). Mirrors `foreach` in 5.1
730/// `ltablib.c`.
731fn foreach(state: &mut LuaState) -> Result<usize, LuaError> {
732 state.check_arg_type(1, LuaType::Table)?;
733 state.check_arg_type(2, LuaType::Function)?;
734 state.push(LuaValue::Nil);
735 while state.table_next(1)? {
736 // Stack: ..., key, value.
737 state.push_value_at(2)?; // function
738 state.push_value_at(-3)?; // key copy
739 state.push_value_at(-3)?; // value copy
740 state.call(2, 1)?;
741 if !matches!(state.type_at(-1), LuaType::Nil) {
742 return Ok(1);
743 }
744 state.pop_n(2); // remove value and result, leaving key for next()
745 }
746 Ok(0)
747}
748
749// ─── Module opener ────────────────────────────────────────────────────────────
750
751/// Builds the `table` library table for the running version. The base roster is
752/// [`TABLE_FUNCS`], from which 5.1 and 5.2 drop the functions they lack and onto
753/// which 5.1's legacy roster and 5.5's `create` are layered. The per-version
754/// deltas below are each verified against that version's reference binary.
755pub fn open_table(state: &mut LuaState) -> Result<usize, LuaError> {
756 // Per-version roster deltas:
757 // - `table.move` is a Lua 5.3 addition, absent in 5.1/5.2 (verified against
758 // lua5.2.4: `type(table.move)` == "nil").
759 // - `table.pack`/`table.unpack` are Lua 5.2 additions; in 5.1 `unpack` is a
760 // *global* and there is no `table.pack` (verified against lua5.1.5: both
761 // `table.unpack` and `table.pack` are nil). 5.1 instead carries the legacy
762 // `getn`/`setn`/`maxn`/`foreach`/`foreachi` roster.
763 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
764 let legacy: Vec<(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)> = TABLE_FUNCS
765 .iter()
766 .filter(|(name, _)| {
767 *name != b"move".as_slice()
768 && *name != b"pack".as_slice()
769 && *name != b"unpack".as_slice()
770 })
771 .copied()
772 .collect();
773 state.new_lib(&legacy)?;
774 const LEGACY_FUNCS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
775 (b"getn", getn),
776 (b"setn", setn),
777 (b"maxn", maxn),
778 (b"foreach", foreach),
779 (b"foreachi", foreachi),
780 ];
781 state.set_funcs_with_upvalues(LEGACY_FUNCS, 0)?;
782 } else if matches!(state.global().lua_version, lua_types::LuaVersion::V52) {
783 let without_move: Vec<(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)> = TABLE_FUNCS
784 .iter()
785 .filter(|(name, _)| *name != b"move".as_slice())
786 .copied()
787 .collect();
788 state.new_lib(&without_move)?;
789 // `table.maxn` survives into 5.2 (it is removed in 5.3). The legacy
790 // `getn`/`setn`/`foreach`/`foreachi` roster, by contrast, is 5.1-only.
791 // Verified against lua5.2.4: `type(table.maxn)` == "function" but
792 // `type(table.getn)` == "nil".
793 const V52_LEGACY: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] =
794 &[(b"maxn", maxn)];
795 state.set_funcs_with_upvalues(V52_LEGACY, 0)?;
796 } else {
797 state.new_lib(TABLE_FUNCS)?;
798 }
799 // Per-version roster delta: `table.create` is a Lua 5.5 addition
800 // (`specs/research/5.5-upstream-delta.md` §5), absent in 5.1-5.4. Register
801 // it only on the V55 backend so the version seam carries a real,
802 // script-observable stdlib difference. `new_lib` leaves the new table on
803 // the stack top, so we register `create` into it directly.
804 if matches!(state.global().lua_version, lua_types::LuaVersion::V55) {
805 const CREATE_FUNCS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] =
806 &[(b"create", create)];
807 state.set_funcs_with_upvalues(CREATE_FUNCS, 0)?;
808 }
809 Ok(1)
810}