diff --git a/.luacheckrc b/.luacheckrc index 82c10fcd3..8121f6f53 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -22,7 +22,7 @@ read_globals = { "PerlinNoise", "PerlinNoiseMap", string = {fields = {"split", "trim"}}, - table = {fields = {"copy", "getn", "indexof", "keyof", "insert_all"}}, + table = {fields = {"copy", "copy_with_metatables", "getn", "indexof", "keyof", "insert_all"}}, math = {fields = {"hypot", "round"}}, } diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua index ce4179f54..47e0aeabc 100644 --- a/builtin/common/misc_helpers.lua +++ b/builtin/common/misc_helpers.lua @@ -457,18 +457,37 @@ do end end --------------------------------------------------------------------------------- -function table.copy(t, seen) - local n = {} - seen = seen or {} - seen[t] = n - for k, v in pairs(t) do - n[(type(k) == "table" and (seen[k] or table.copy(k, seen))) or k] = - (type(v) == "table" and (seen[v] or table.copy(v, seen))) or v + +local function table_copy(value, preserve_metatables) + local seen = {} + local function copy(val) + if type(val) ~= "table" then + return val + end + local t = val + if seen[t] then + return seen[t] + end + local res = {} + seen[t] = res + for k, v in pairs(t) do + res[copy(k)] = copy(v) + end + if preserve_metatables then + setmetatable(res, getmetatable(t)) + end + return res end - return n + return copy(value) end +function table.copy(value) + return table_copy(value, false) +end + +function table.copy_with_metatables(value) + return table_copy(value, true) +end function table.insert_all(t, other) if table.move then -- LuaJIT diff --git a/builtin/common/tests/misc_helpers_spec.lua b/builtin/common/tests/misc_helpers_spec.lua index 10e2bf277..d24fb0c8f 100644 --- a/builtin/common/tests/misc_helpers_spec.lua +++ b/builtin/common/tests/misc_helpers_spec.lua @@ -178,6 +178,35 @@ describe("table", function() assert.equal(2, table.keyof({[2] = "foo", [3] = "bar"}, "foo")) assert.equal(3, table.keyof({[1] = "foo", [3] = "bar"}, "bar")) end) + + describe("copy()", function() + it("strips metatables", function() + local v = vector.new(1, 2, 3) + local w = table.copy(v) + assert.are_not.equal(v, w) + assert.same(v, w) + assert.equal(nil, getmetatable(w)) + end) + it("preserves referential structure", function() + local t = {{}, {}} + t[1][1] = t[2] + t[2][1] = t[1] + local copy = table.copy(t) + assert.same(t, copy) + assert.equal(copy[1][1], copy[2]) + assert.equal(copy[2][1], copy[1]) + end) + end) + + describe("copy_with_metatables()", function() + it("preserves metatables", function() + local v = vector.new(1, 2, 3) + local w = table.copy_with_metatables(v) + assert.equal(getmetatable(v), getmetatable(w)) + assert(vector.check(w)) + assert.equal(v, w) -- vector overrides == + end) + end) end) describe("formspec_escape", function() diff --git a/doc/lua_api.md b/doc/lua_api.md index facb20556..ec10458a7 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -4142,6 +4142,11 @@ Helper functions * returns time with microsecond precision. May not return wall time. * `table.copy(table)`: returns a table * returns a deep copy of `table` + * strips metatables, but this may change in the future +* `table.copy_with_metatables(table)` + * since 5.12 + * `table` can also be non-table value, which will be returned as-is + * preserves metatables as they are * `table.indexof(list, val)`: returns the smallest numerical index containing the value `val` in the table `list`. Non-numerical indices are ignored. If `val` could not be found, `-1` is returned. `list` must not have