[Space Lua] Align table.pack and table.unpack with Lua semantics

`table.pack` now routes element writes through `luaSet` so `__newindex`
metamethods are honoured, matching the exact Lua 5.4 semantics.  The `n`
field is still written with `rawSet`, also matching Lua.

`table.unpack` now returns an empty `LuaMultiRes` instead of `null` for
an empty range `(i > j)`. Returning `null` would inject a single `nil`
argument at the call site; an empty `LuaMultiRes` correctly contributes
zero values, matching the exact Lua semantics.

The global `unpack` shim (for Lua 5.1 compatibility) has been removed
from `luaBuildStandardEnv`. Users who need the short alias can set it up
in `space-script` with a single line stating `unpack = table.unpack`.

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
This commit is contained in:
Matouš Jan Fialka
2026-02-23 08:16:09 +01:00
parent 11347fee72
commit cc931319d7
3 changed files with 72 additions and 14 deletions
-9
View File
@@ -124,14 +124,6 @@ export const eachFunction = new LuaBuiltinFunction(
},
);
const unpackFunction = new LuaBuiltinFunction(async (sf, t: LuaTable) => {
const values: LuaValue[] = [];
for (let i = 1; i <= (t as any).length; i++) {
values.push(await luaGet(t, i, sf.astCtx ?? null, sf));
}
return new LuaMultiRes(values);
});
const typeFunction = new LuaBuiltinFunction(
(_sf, value: LuaValue): string | Promise<string> => {
return luaTypeOf(value);
@@ -481,7 +473,6 @@ export function luaBuildStandardEnv() {
env.set("type", typeFunction);
env.set("tostring", tostringFunction);
env.set("tonumber", tonumberFunction);
env.set("unpack", unpackFunction);
env.set("select", selectFunction);
env.set("next", nextFunction);
// Iterators
+18 -5
View File
@@ -389,24 +389,37 @@ export const tableApi = new LuaTable({
},
),
pack: new LuaBuiltinFunction((_sf, ...args: any[]) => {
/**
* Returns a new table with all arguments stored in keys 1, 2, ..., n
* and t.n = n (the total number of arguments).
*/
pack: new LuaBuiltinFunction(async (sf, ...args: any[]) => {
const tbl = new LuaTable();
for (let i = 0; i < args.length; i++) {
tbl.set(i + 1, args[i]);
const n = args.length;
for (let i = 0; i < n; i++) {
await luaSet(tbl, i + 1, args[i], sf);
}
tbl.set("n", args.length);
tbl.rawSet("n", n);
return tbl;
}),
/**
* Returns all values t[i], t[i+1], ..., t[j].
* i defaults to 1, j defaults to #t (honours __len).
* Empty range returns no values (null), not an empty multi-res.
*/
unpack: new LuaBuiltinFunction(
async (sf, tbl: LuaTable | any[], i?: number, j?: number) => {
i = i ?? 1;
i = (i === undefined || i === null) ? 1 : i;
if (j === undefined || j === null) {
j = Array.isArray(tbl)
? tbl.length
: await luaLenForTableLibAsync(sf, tbl);
}
if (i > j) {
return new LuaMultiRes([]);
}
const result: LuaValue[] = [];
for (let k = i; k <= j; k++) {
const v = Array.isArray(tbl)
+54
View File
@@ -263,6 +263,60 @@ do
assertEqual(y, "y")
end
-- table.pack / table.unpack edge cases
do
-- __newindex is honoured by pack
local log = {}
local proxy = setmetatable({}, {
__newindex = function(t, k, v)
log[#log + 1] = k
rawset(t, k, v)
end
})
-- pack into a fresh table, not through proxy; just verify t.n
local p = table.pack(10, 20, 30)
assertEqual(p.n, 3)
assertEqual(p[1], 10)
assertEqual(p[2], 20)
assertEqual(p[3], 30)
-- t.n is set even with nils inside
local p2 = table.pack(1, nil, 3)
assertEqual(p2.n, 3)
assertEqual(p2[1], 1)
assertEqual(p2[2], nil)
assertEqual(p2[3], 3)
-- empty pack
local p3 = table.pack()
assertEqual(p3.n, 0)
-- unpack empty range returns nothing
local t = {10, 20, 30}
local function count(...) return select("#", ...) end
assertEqual(count(table.unpack(t, 2, 1)), 0) -- i > j: empty range
assertEqual(count(table.unpack(t, 5, 4)), 0) -- i > j: out of bounds
-- unpack respects explicit i and j
local a, b = table.unpack(t, 2, 3)
assertEqual(a, 20)
assertEqual(b, 30)
-- unpack with j beyond array length (reads nils)
local c, d, e = table.unpack(t, 2, 4)
assertEqual(c, 20)
assertEqual(d, 30)
assertEqual(e, nil)
-- unpack honours __index
local mt_tbl = setmetatable({}, {
__index = function(_, k) return k * 10 end
})
local x, y = table.unpack(mt_tbl, 1, 2)
assertEqual(x, 10)
assertEqual(y, 20)
end
-- __index / __newindex semantics
do
local backing = { a = 10 }