Skip to main content

luau_vm/libs/table/
mod.rs

1use core::ptr;
2
3use luau_common::ByteSlice;
4
5use crate::Table;
6use crate::VmResult;
7use crate::call::ThreadStack;
8use crate::debug::DebugRuntime;
9use crate::gc::GcBarrier;
10use crate::gc::GcObject;
11use crate::handle::RawHandle;
12use crate::native::{NativeCallContext, NativeCallResult, NativeFunction};
13use crate::state::ThreadState;
14use crate::string::StringRuntime;
15use crate::table::TableRuntime;
16use crate::thread::stack::RawStackAccess;
17use crate::thread::{LuaStringBuilder, LuaStringBuilderStorage, Thread};
18use crate::types::{LUA_TFUNCTION, LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE};
19use crate::vm::VmOperations;
20
21mod sort;
22
23use sort::table_sort;
24
25static TABLE_FUNCS: [NativeFunction; 17] = [
26    NativeFunction {
27        name: "concat",
28        function: table_concat,
29    },
30    NativeFunction {
31        name: "foreach",
32        function: table_for_each,
33    },
34    NativeFunction {
35        name: "foreachi",
36        function: table_for_each_i,
37    },
38    NativeFunction {
39        name: "getn",
40        function: table_getn,
41    },
42    NativeFunction {
43        name: "maxn",
44        function: table_maxn,
45    },
46    NativeFunction {
47        name: "insert",
48        function: table_insert,
49    },
50    NativeFunction {
51        name: "remove",
52        function: table_remove,
53    },
54    NativeFunction {
55        name: "sort",
56        function: table_sort,
57    },
58    NativeFunction {
59        name: "pack",
60        function: table_pack,
61    },
62    NativeFunction {
63        name: "unpack",
64        function: table_unpack,
65    },
66    NativeFunction {
67        name: "move",
68        function: table_move,
69    },
70    NativeFunction {
71        name: "create",
72        function: table_create,
73    },
74    NativeFunction {
75        name: "find",
76        function: table_find,
77    },
78    NativeFunction {
79        name: "clear",
80        function: table_clear,
81    },
82    NativeFunction {
83        name: "freeze",
84        function: table_freeze,
85    },
86    NativeFunction {
87        name: "isfrozen",
88        function: table_is_frozen,
89    },
90    NativeFunction {
91        name: "clone",
92        function: table_clone,
93    },
94];
95
96fn table_argument(thread: &Thread, argument: i32) -> VmResult<Table> {
97    unsafe { thread.check_type(argument, LUA_TTABLE)? };
98    Ok(unsafe { thread.to_object(argument).unwrap_unchecked().table_value() })
99}
100
101/// `moveelements`
102unsafe fn move_elements(
103    thread: &Thread,
104    src_index: i32,
105    dst_index: i32,
106    first: i32,
107    last: i32,
108    target: i32,
109) -> VmResult {
110    let src = table_argument(thread, src_index)?;
111    let dst = table_argument(thread, dst_index)?;
112
113    unsafe {
114        if dst.as_ptr().as_ref().unwrap_unchecked().readonly != 0 {
115            return thread.readonly_error().map_err(Into::into);
116        }
117
118        let count = last - first + 1;
119        let src_size = src.as_ptr().as_ref().unwrap_unchecked().size_array;
120        let dst_size = dst.as_ptr().as_ref().unwrap_unchecked().size_array;
121
122        if (first as u32).wrapping_sub(1) < src_size as u32
123            && (target as u32).wrapping_sub(1) < dst_size as u32
124            && (first as u32).wrapping_sub(1).wrapping_add(count as u32) <= src_size as u32
125            && (target as u32).wrapping_sub(1).wrapping_add(count as u32) <= dst_size as u32
126        {
127            if count > 0 {
128                let src_array = src.array_cursor().add((first - 1) as usize).as_ptr();
129                let dst_array = dst.array_cursor().add((target - 1) as usize).as_ptr();
130                ptr::copy(src_array, dst_array, count as usize);
131            }
132
133            let dst_object: GcObject = dst.into();
134            if dst_object.is_black() {
135                thread.barrier_back(
136                    dst_object,
137                    &raw mut dst.as_ptr().as_mut().unwrap_unchecked().gc_list,
138                );
139            }
140        } else if target > last || target <= first || dst != src {
141            for i in 0..count {
142                thread.raw_geti(src_index, first + i)?;
143                thread.raw_seti(dst_index, target + i)?;
144            }
145        } else {
146            for i in (0..count).rev() {
147                thread.raw_geti(src_index, first + i)?;
148                thread.raw_seti(dst_index, target + i)?;
149            }
150        }
151    }
152    Ok(())
153}
154
155/// `addfield`
156unsafe fn add_field(
157    thread: &Thread,
158    buffer: &mut LuaStringBuilder<'_, '_>,
159    index: i32,
160    table: Table,
161) -> VmResult {
162    unsafe {
163        if (index as u32).wrapping_sub(1)
164            < table.as_ptr().as_ref().unwrap_unchecked().size_array as u32
165        {
166            let entry = table.array_slot((index - 1) as usize);
167            if entry.is_string() {
168                buffer.push_bytes(entry.string_value().as_bytes())?;
169                return Ok(());
170            }
171        }
172
173        let value_type = thread.raw_geti(1, index)?;
174        if value_type != LUA_TSTRING && value_type != LUA_TNUMBER {
175            let type_name = thread.lua_type_name(-1);
176            let message = luau_printf::sprintf!(
177                "invalid value (%s) at index %d in table for 'concat'",
178                type_name.as_bstr(),
179                index
180            );
181            return crate::error!(thread, &message).map_err(Into::into);
182        }
183
184        buffer.push_stack_value()?;
185    }
186    Ok(())
187}
188
189/// `maxn`
190fn table_maxn(ctx: NativeCallContext) -> NativeCallResult {
191    let thread = ctx.raw_thread();
192    let max = unsafe {
193        thread.check_type(1, LUA_TTABLE)?;
194
195        let table = thread.to_object(1).unwrap_unchecked().table_value();
196        let mut max = 0.0f64;
197
198        for i in 0..table.as_ptr().as_ref().unwrap_unchecked().size_array {
199            if !table.array_slot(i as usize).is_nil() {
200                max = (i + 1) as f64;
201            }
202        }
203
204        for i in 0..table.node_count() {
205            let node = table.node(i as i32);
206            if !node.value_unchecked().is_nil() && node.key().tt() == LUA_TNUMBER {
207                let value = node.key().number_value();
208                if value > max {
209                    max = value;
210                }
211            }
212        }
213
214        max
215    };
216    ctx.push_number(max)?;
217    Ok(1)
218}
219
220/// `getn`
221fn table_getn(ctx: NativeCallContext) -> NativeCallResult {
222    let thread = ctx.raw_thread();
223    unsafe { thread.check_type(1, LUA_TTABLE)? };
224    ctx.push_integer(unsafe { thread.obj_len(1) })?;
225    Ok(1)
226}
227
228/// `tinsert`
229fn table_insert(ctx: NativeCallContext) -> NativeCallResult {
230    let thread = ctx.raw_thread();
231    unsafe {
232        thread.check_type(1, LUA_TTABLE)?;
233        let n = thread.obj_len(1);
234
235        let pos = match thread.get_top() {
236            2 => n + 1,
237            3 => {
238                let pos = thread.check_integer(2)?;
239                if (1..=n).contains(&pos) {
240                    move_elements(thread, 1, 1, pos, n, pos + 1)?;
241                }
242                pos
243            }
244            _ => {
245                return crate::error!(thread, "wrong number of arguments to 'insert'")
246                    .map_err(Into::into);
247            }
248        };
249
250        thread.raw_seti(1, pos)?;
251        Ok(0)
252    }
253}
254
255/// `tremove`
256fn table_remove(ctx: NativeCallContext) -> NativeCallResult {
257    let thread = ctx.raw_thread();
258    unsafe {
259        thread.check_type(1, LUA_TTABLE)?;
260        let n = thread.obj_len(1);
261        let pos = thread.opt_integer(2, n)?;
262
263        if !(1..=n).contains(&pos) {
264            return Ok(0);
265        }
266
267        thread.raw_geti(1, pos)?;
268        move_elements(thread, 1, 1, pos + 1, n, pos)?;
269        thread.push_nil()?;
270        thread.raw_seti(1, n)?;
271        Ok(1)
272    }
273}
274
275/// `tmove`
276fn table_move(ctx: NativeCallContext) -> NativeCallResult {
277    let thread = ctx.raw_thread();
278    let dst_index = unsafe {
279        thread.check_type(1, LUA_TTABLE)?;
280        let first = thread.check_integer(2)?;
281        let last = thread.check_integer(3)?;
282        let target = thread.check_integer(4)?;
283        let dst_index = if thread.is_none_or_nil(5) != 0 { 1 } else { 5 };
284        thread.check_type(dst_index, LUA_TTABLE)?;
285
286        if last >= first {
287            if first <= 0 && last >= i32::MAX + first {
288                return thread
289                    .lua_arg_error(3, "too many elements to move")
290                    .map_err(Into::into);
291            }
292
293            let count = last - first + 1;
294            if target > i32::MAX - count + 1 {
295                return thread
296                    .lua_arg_error(4, "destination wrap around")
297                    .map_err(Into::into);
298            }
299
300            let dst = thread.to_object(dst_index).unwrap_unchecked().table_value();
301            if dst.as_ptr().as_ref().unwrap_unchecked().readonly != 0 {
302                return thread.readonly_error().map_err(Into::into);
303            }
304
305            if target > 0
306                && (target - 1) <= dst.as_ptr().as_ref().unwrap_unchecked().size_array
307                && (target - 1 + count) > dst.as_ptr().as_ref().unwrap_unchecked().size_array
308            {
309                thread.resize_array(dst, target - 1 + count)?;
310            }
311
312            move_elements(thread, 1, dst_index, first, last, target)?;
313        }
314
315        dst_index
316    };
317
318    unsafe { thread.push_value(dst_index)? };
319    Ok(1)
320}
321
322/// `tconcat`
323fn table_concat(ctx: NativeCallContext) -> NativeCallResult {
324    let thread = ctx.raw_thread();
325    unsafe {
326        let separator = thread.opt_string(2)?.unwrap_or(b"".as_bstr());
327        thread.check_type(1, LUA_TTABLE)?;
328        let mut index = thread.opt_integer(3, 1)?;
329        let last = if thread.is_none_or_nil(4) != 0 {
330            thread.obj_len(1)
331        } else {
332            thread.check_integer(4)?
333        };
334        let table = thread.to_object(1).unwrap_unchecked().table_value();
335        let mut buffer_storage = LuaStringBuilderStorage::uninit();
336        let mut buffer = LuaStringBuilder::new(thread, &mut buffer_storage);
337
338        while index < last {
339            add_field(thread, &mut buffer, index, table)?;
340            if !separator.is_empty() {
341                buffer.push_bytes(separator)?;
342            }
343            index += 1;
344        }
345
346        if index == last {
347            add_field(thread, &mut buffer, index, table)?;
348        }
349
350        buffer.finish()?;
351        Ok(1)
352    }
353}
354
355/// `foreachi`
356fn table_for_each_i(ctx: NativeCallContext) -> NativeCallResult {
357    let thread = ctx.raw_thread();
358    unsafe {
359        thread.check_type(1, LUA_TTABLE)?;
360        thread.check_type(2, LUA_TFUNCTION)?;
361        let n = thread.obj_len(1);
362        for index in 1..=n {
363            thread.push_value(2)?;
364            thread.push_integer(index)?;
365            thread.raw_geti(1, index)?;
366            thread.call(2, 1)?;
367
368            if thread.is_nil(-1) == 0 {
369                return Ok(1);
370            }
371
372            thread.pop(1);
373        }
374
375        Ok(0)
376    }
377}
378
379/// `foreach`
380fn table_for_each(ctx: NativeCallContext) -> NativeCallResult {
381    let thread = ctx.raw_thread();
382    unsafe {
383        thread.check_type(1, LUA_TTABLE)?;
384        thread.check_type(2, LUA_TFUNCTION)?;
385        thread.push_nil()?;
386        while thread.next(1)? != 0 {
387            thread.push_value(2)?;
388            thread.push_value(-3)?;
389            thread.push_value(-3)?;
390            thread.call(2, 1)?;
391
392            if thread.is_nil(-1) == 0 {
393                return Ok(1);
394            }
395
396            thread.pop(2);
397        }
398
399        Ok(0)
400    }
401}
402
403/// `tpack`
404fn table_pack(ctx: NativeCallContext) -> NativeCallResult {
405    let thread = ctx.raw_thread();
406    unsafe {
407        let n = thread.get_top();
408        thread.create_table(n as usize, 1)?;
409
410        let table = thread.to_object(-1).unwrap_unchecked().table_value();
411        let array = table.array_cursor();
412        let base = thread.stack_base();
413        for i in 0..n as usize {
414            array
415                .add(i)
416                .value_unchecked()
417                .set_obj(base.add(i).value_unchecked());
418        }
419
420        let key = thread.intern_string(b"n".as_bstr())?;
421        let node_cursor = thread.set_str(table, key)?;
422        node_cursor
423            .node_unchecked()
424            .value_unchecked()
425            .set_number(n as f64);
426        Ok(1)
427    }
428}
429
430/// `tunpack`
431fn table_unpack(ctx: NativeCallContext) -> NativeCallResult {
432    let thread = ctx.raw_thread();
433    unsafe {
434        thread.check_type(1, LUA_TTABLE)?;
435        let table = thread.to_object(1).unwrap_unchecked().table_value();
436        let start = thread.opt_integer(2, 1)?;
437        let end = if thread.is_none_or_nil(3) != 0 {
438            thread.obj_len(1)
439        } else {
440            thread.check_integer(3)?
441        };
442
443        if start > end {
444            return Ok(0);
445        }
446
447        let n = (end as u32).wrapping_sub(start as u32) as i32 + 1;
448        if n <= 0 || thread.check_stack(n) == 0 {
449            return crate::error!(thread, "too many results to unpack").map_err(Into::into);
450        }
451
452        if start == 1 && n <= table.as_ptr().as_ref().unwrap_unchecked().size_array {
453            let top = thread.stack_top();
454            for i in 0..n as usize {
455                top.add(i).value_unchecked().set_obj(table.array_slot(i));
456            }
457            thread.expand_stack_limit(top.add(n as usize));
458            thread.set_stack_top(top.add(n as usize));
459        } else {
460            for i in start..end {
461                thread.raw_geti(1, i)?;
462            }
463            thread.raw_geti(1, end)?;
464        }
465
466        Ok(n as usize)
467    }
468}
469
470/// `tcreate`
471fn table_create(ctx: NativeCallContext) -> NativeCallResult {
472    let thread = ctx.raw_thread();
473    unsafe {
474        let size = thread.check_integer(1)?;
475        if size < 0 {
476            return thread
477                .lua_arg_error(1, "size out of range")
478                .map_err(Into::into);
479        }
480
481        if thread.is_none_or_nil(2) == 0 {
482            let value = thread.stack_base().add(1).value_unchecked();
483            thread.create_table(size as usize, 0)?;
484            let table = thread.to_object(-1).unwrap_unchecked().table_value();
485            let array = table.array_cursor();
486            for i in 0..size as usize {
487                array.add(i).value_unchecked().set_obj(value);
488            }
489        } else {
490            thread.create_table(size as usize, 0)?;
491        }
492
493        Ok(1)
494    }
495}
496
497/// `tfind`
498fn table_find(ctx: NativeCallContext) -> NativeCallResult {
499    let thread = ctx.raw_thread();
500    unsafe {
501        thread.check_type(1, LUA_TTABLE)?;
502        thread.check_any(2)?;
503
504        let init = thread.opt_integer(3, 1)?;
505        if init < 1 {
506            return thread
507                .lua_arg_error(3, "index out of range")
508                .map_err(Into::into);
509        }
510
511        let table = thread.to_object(1).unwrap_unchecked().table_value();
512        let needle = thread.stack_base().add(1).value_unchecked();
513
514        for index in init.. {
515            let entry = table.get_num(index);
516            if entry.is_nil() {
517                break;
518            }
519
520            let equal = if entry.tt() == needle.tt() {
521                thread.equal_value(entry, needle)? != 0
522            } else {
523                false
524            };
525
526            if equal {
527                thread.push_integer(index)?;
528                return Ok(1);
529            }
530        }
531
532        thread.push_nil()?;
533    }
534    Ok(1)
535}
536
537/// `tclear`
538fn table_clear(ctx: NativeCallContext) -> NativeCallResult {
539    let thread = ctx.raw_thread();
540    unsafe { thread.check_type(1, LUA_TTABLE)? };
541    unsafe { thread.clear_table(1)? };
542    Ok(0)
543}
544
545/// `tfreeze`
546fn table_freeze(ctx: NativeCallContext) -> NativeCallResult {
547    let thread = ctx.raw_thread();
548    unsafe {
549        thread.check_type(1, LUA_TTABLE)?;
550        if thread.get_readonly(1) != 0 {
551            return thread
552                .lua_arg_error(1, "table is already frozen")
553                .map_err(Into::into);
554        }
555        if thread.get_metafield(1, "__metatable")? != 0 {
556            return thread
557                .lua_arg_error(1, "table has a protected metatable")
558                .map_err(Into::into);
559        }
560
561        thread.set_readonly(1, 1);
562        thread.push_value(1)?;
563        Ok(1)
564    }
565}
566
567/// `tisfrozen`
568fn table_is_frozen(ctx: NativeCallContext) -> NativeCallResult {
569    let thread = ctx.raw_thread();
570    unsafe { thread.check_type(1, LUA_TTABLE)? };
571    unsafe { thread.push_boolean(thread.get_readonly(1))? };
572    Ok(1)
573}
574
575/// `tclone`
576fn table_clone(ctx: NativeCallContext) -> NativeCallResult {
577    let thread = ctx.raw_thread();
578    unsafe {
579        thread.check_type(1, LUA_TTABLE)?;
580        if thread.get_metafield(1, "__metatable")? != 0 {
581            return thread
582                .lua_arg_error(1, "table has a protected metatable")
583                .map_err(Into::into);
584        }
585
586        thread.clone_table(1)?;
587        Ok(1)
588    }
589}
590
591impl Thread {
592    /// `luaopen_table`
593    pub unsafe fn open_table(&self) -> NativeCallResult {
594        unsafe { self.register(Some(super::LUA_TABLIB_NAME), &TABLE_FUNCS[..])? };
595
596        // Lua 5.1 compatibility global.
597        unsafe {
598            self.push_native_function(table_unpack, Some("unpack"))?;
599            self.set_global("unpack")?;
600        }
601
602        Ok(1)
603    }
604}