diff --git a/liluat.lua b/liluat.lua index 0e9e194..21c4d47 100644 --- a/liluat.lua +++ b/liluat.lua @@ -62,44 +62,127 @@ local function escape_pattern(text) end liluat.private.escape_pattern = escape_pattern --- recursively copy a table -local function clone_table(table) - local clone = {} +-- replace pairs with a version that doesn't respect __pairs metamethods +local function pairs(table) + return next, table +end - for key, value in pairs(table) do - if type(value) == "table" then - clone[key] = clone_table(value) - else - clone[key] = value +-- make a deep copy of a table using breadth first search. +-- supports cycles +local function bfs_clone(tab, coloring, parents) + local queue = {} + local coloring = coloring or {} + + -- color the table + local copied_tab = {} + coloring[tab] = copied_tab + + -- queue up the table + table.insert(queue, tab) + + local node = nil + repeat + node = table.remove(queue, 1) + local copied_node = coloring[node] + local copied_key = nil + local copied_value = nil + + for key,value in pairs(node) do + copied_key = coloring[key] + if copied_key == nil then -- new node + if type(key) == "table" then + copied_key = {} + table.insert(queue, key) + else + copied_key = key + end + + coloring[key] = copied_key + if parents then + parents[copied_key] = {[copied_node] = "key"} + end + elseif parents then + parents[copied_key][copied_node] = "key" + end + + copied_value = coloring[value] + if copied_value == nil then -- new node + if type(value) == "table" then + copied_value = {} + table.insert(queue, value) + else + copied_value = value + end + + coloring[value] = copied_value + if parents then + parents[copied_value] = {[copied_node] = "key"} + end + elseif parents then + parents[copied_value][copied_node] = "value" + end + + -- put key and value in the copy + rawset(copied_node, copied_key, copied_value) end - end - return clone + local metatable = debug.getmetatable(node) + if metatable then + local copied_metatable = coloring[metatable] + if copied_metatable == nil then -- new node + copied_metatable = {} + coloring[metatable] = copied_metatable + if parents then + parents[copied_metatable] = {[copied_node] = "metatable"} + end + table.insert(queue, metatable) + elseif parents then + parents[copied_metatable][copied_node] = "metatable" + end + + debug.setmetatable(copied_node, copied_metatable) + end + until #queue == 0 + + return copied_tab end -liluat.private.clone_table = clone_table +liluat.private.bfs_clone = bfs_clone -- recursively merge two tables, the second one has precedence -- if 'shallow' is set, the second table isn't copied recursively, -- its content is only referenced instead -local function merge_tables(a, b, shallow) - a = a or {} - b = b or {} +local function merge_tables(a, b, shallow, merge_coloring) + -- TODO find more efficient algorithm to do this + if not merge_coloring then + a = a or {} + b = b or {} + + local coloring = {} + a = bfs_clone(a, coloring) + if not shallow then + b = bfs_clone(b, coloring) + end + coloring = nil -- free after use - local merged = clone_table(a) + merge_coloring = {} + end for key, value in pairs(b) do - if (type(value) == "table") and (not shallow) then - if a[key] then - merged[key] = merge_tables(a[key], value) + if (type(value) == "table") and (not merge_coloring[value]) and (not shallow) then + local original = rawget(a, key) + if type(original) == "table" then + debug.setmetatable(original, debug.getmetatable(value)) + merge_coloring[value] = true + merge_tables(original, value, shallow, merge_coloring) else - merged[key] = clone_table(value) + rawset(a, key, value) end else - merged[key] = value + rawset(a, key, value) end end - return merged + return a end liluat.private.merge_tables = merge_tables diff --git a/spec/liluat_spec.lua b/spec/liluat_spec.lua index c5b6350..20f0b4e 100644 --- a/spec/liluat_spec.lua +++ b/spec/liluat_spec.lua @@ -169,7 +169,7 @@ Hello, <world>! end) end) - describe("clone_table", function () + describe("bfs_clone", function () it("should clone a table", function () local table = { a = { @@ -181,13 +181,125 @@ Hello, <world>! e = 3 } - local clone = liluat.private.clone_table(table) + local clone = liluat.private.bfs_clone(table) assert.same(table, clone) assert.not_equal(table, clone) assert.not_equal(table.a, clone.a) assert.not_equal(table.a.c, clone.a.c) end) + + it("should clone a table with cycles", function () + local table = { + a = 1 + } + table.b = table + table[table] = table + + local clone = liluat.private.bfs_clone(table) + + assert.not_equal(table, clone) + assert.equal(table.a, clone.a) + assert.equal(clone[clone], clone) + assert.equal(clone.b, clone) + end) + + it("should clone a metatable", function () + local table = {table = true} + local metatable = {metatable = true} + setmetatable(table, metatable) + + local clone = liluat.private.bfs_clone(table) + + assert.not_equal(table, clone) + assert.is_true(clone.table) + assert.is_table(getmetatable(clone)) + assert.not_equal(getmetatable(clone), getmetatable(table)) + assert.is_true(getmetatable(clone).metatable) + end) + + it("should clone a table and it's metatables", function () + local a = {test = true} + local b = {test = false} + setmetatable(a, b) + setmetatable(b, a) + + local table = { + [4] = 5, + a = a, + b = b, + [a] = a, + [b] = a + } + + table[table] = table + + local clone = liluat.private.bfs_clone(table) + + assert.not_equal(table, clone) + assert.not_equal(table.a, clone.a) + assert.not_equal(table.b, clone.b) + assert.equal(5, clone[4]) + assert.not_equal(clone.a, clone.b) + assert.equal(clone.a, clone[clone.a]) + assert.equal(clone.a, clone[clone.b]) + assert.equal(clone, clone[clone]) + assert.equal(clone.a, getmetatable(clone.b)) + assert.equal(clone.b, getmetatable(clone.a)) + end) + + it("should clone tables with __pairs in their metatable", function () + local pairs = function () + return nil + end + + local table = {a = 1} + local metatable = {__pairs = pairs} + setmetatable(table, metatable) + + local clone = liluat.private.bfs_clone(table) + + assert.not_equal(table, clone) + assert.equal(table.a, clone.a) + assert.equal(pairs, getmetatable(clone).__pairs) + end) + + it("should clone tables with __index and __newindex metamethods", function () + local index = function (table, key, value) + return true + end + + local newindex = function (table, key, value) + rawset(table, key, false) + end + + local table = {a = 1} + local metatable = {__index = index, __newindex = newindex} + setmetatable(table, metatable) + + local clone = liluat.private.bfs_clone(table) + + assert.not_equal(table, clone) + assert.equal(table.a, clone.a) + assert.not_equal(getmetatable(table), getmetatable(clone)) + assert.is_true(clone.something) + clone.bla = 4 + assert.is_false(clone.bla) + end) + + it("should clone tables with __metatable in their metatable", function () + local table = {} + local metatable = {__metatable = false} + setmetatable(table, metatable) + + local clone = liluat.private.bfs_clone(table) + + assert.not_equal(table, clone) + assert.is_false(getmetatable(clone)) + assert.not_equal(debug.getmetatable(table), debug.getmetatable(clone)) + assert.truthy(debug.getmetatable(clone)) + assert.is_false(debug.getmetatable(clone).__metatable) + end) end) describe("merge_tables", function () @@ -297,6 +409,25 @@ Hello, <world>! assert.same({a = 1}, liluat.private.merge_tables(a, nil)) assert.same({}, liluat.private.merge_tables(nil, nil)) end) + + pending("should merge a table with cycles", function () + local table = {a = 1} + + local c = {c = true} + local b = {c = c, b = false} + local a = {b = b} + c.a = a + + local merged = liluat.private.merge_tables(table, a) + + assert.not_equal(a1, merged) + assert.not_equal(a2, merged) + assert.equal(merged, merged.b.c.a) + + end) + + pending("should join common subtables when merging", function () + end) end) describe("escape_pattern", function ()