From 9c58741a1c1ef4c9353e6d03cf5c2b97eea175a1 Mon Sep 17 00:00:00 2001 From: Max Bruckner Date: Tue, 10 May 2016 19:12:57 +0200 Subject: [PATCH 1/8] new function bfs_clone that clones a table with breadth first search This enables copying tables with cycles. Still lacks support for copyiing metatables. --- liluat.lua | 46 ++++++++++++++++++++++++++++++++++++++++++++ spec/liluat_spec.lua | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/liluat.lua b/liluat.lua index 0e9e194..4243a29 100644 --- a/liluat.lua +++ b/liluat.lua @@ -62,6 +62,52 @@ local function escape_pattern(text) end liluat.private.escape_pattern = escape_pattern +-- make a deep copy of a table using breadth first search. +-- supports cycles +local function bfs_clone(tab) + local queue = {} + local coloring = {} + + -- color the table + coloring[tab] = {} + -- queue up the table + table.insert(queue, tab) + + local node = nil + repeat + node = table.remove(queue, 1) + + for key,value in pairs(node) do + if coloring[key] == nil then -- new node + if type(key) == "table" then + coloring[key] = {} + table.insert(queue, key) + else + coloring[key] = key + end + end + + if coloring[value] == nil then -- new node + if type(value) == "table" then + coloring[value] = {} + table.insert(queue, value) + else + coloring[value] = value + end + end + + -- put key and value in the copy + local copied_key = coloring[key] + local copied_value = coloring[value] + local copied_node = coloring[node] + copied_node[copied_key] = copied_value + end + until #queue == 0 + + return coloring[tab] +end +liluat.private.bfs_clone = bfs_clone + -- recursively copy a table local function clone_table(table) local clone = {} diff --git a/spec/liluat_spec.lua b/spec/liluat_spec.lua index c5b6350..adcce41 100644 --- a/spec/liluat_spec.lua +++ b/spec/liluat_spec.lua @@ -190,6 +190,42 @@ Hello, <world>! end) end) + describe("bfs_clone", function () + it("should clone a table", function () + local table = { + a = { + b = 1, + c = { + d = 2 + } + }, + e = 3 + } + + 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) + end) + describe("merge_tables", function () it("should merge two tables", function () local a = { From 7f33e5cf6cb7c9f116eacc5696cfc3ec386587c4 Mon Sep 17 00:00:00 2001 From: Max Bruckner Date: Tue, 10 May 2016 19:31:15 +0200 Subject: [PATCH 2/8] bfs_clone: copy metatables --- liluat.lua | 10 ++++++++++ spec/liluat_spec.lua | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/liluat.lua b/liluat.lua index 4243a29..df6fb8a 100644 --- a/liluat.lua +++ b/liluat.lua @@ -102,6 +102,16 @@ local function bfs_clone(tab) local copied_node = coloring[node] copied_node[copied_key] = copied_value end + + local metatable = getmetatable(node) + if metatable then + if coloring[metatable] == nil then -- new node + coloring[metatable] = {} + table.insert(queue, metatable) + end + + setmetatable(coloring[node], coloring[metatable]) + end until #queue == 0 return coloring[tab] diff --git a/spec/liluat_spec.lua b/spec/liluat_spec.lua index adcce41..58d35e1 100644 --- a/spec/liluat_spec.lua +++ b/spec/liluat_spec.lua @@ -224,6 +224,50 @@ Hello, <world>! 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) end) describe("merge_tables", function () From 329370b9f9887cd1cf6ec6095914fc9f6776d4d3 Mon Sep 17 00:00:00 2001 From: Max Bruckner Date: Tue, 10 May 2016 19:39:39 +0200 Subject: [PATCH 3/8] bfs_clone: support tables with __pairs metamethod --- liluat.lua | 4 ++++ spec/liluat_spec.lua | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/liluat.lua b/liluat.lua index df6fb8a..9839fe5 100644 --- a/liluat.lua +++ b/liluat.lua @@ -68,6 +68,10 @@ local function bfs_clone(tab) local queue = {} local coloring = {} + local function pairs(table) + return next, table + end + -- color the table coloring[tab] = {} -- queue up the table diff --git a/spec/liluat_spec.lua b/spec/liluat_spec.lua index 58d35e1..dfd0e4f 100644 --- a/spec/liluat_spec.lua +++ b/spec/liluat_spec.lua @@ -268,6 +268,22 @@ Hello, <world>! 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) end) describe("merge_tables", function () From 2c7a4d00542257add2f86cdbf5360b6b30a51af6 Mon Sep 17 00:00:00 2001 From: Max Bruckner Date: Tue, 10 May 2016 20:02:53 +0200 Subject: [PATCH 4/8] bfs_clone: support for tables with __index or __newindex metamethods --- liluat.lua | 2 +- spec/liluat_spec.lua | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/liluat.lua b/liluat.lua index 9839fe5..e8a6e48 100644 --- a/liluat.lua +++ b/liluat.lua @@ -104,7 +104,7 @@ local function bfs_clone(tab) local copied_key = coloring[key] local copied_value = coloring[value] local copied_node = coloring[node] - copied_node[copied_key] = copied_value + rawset(copied_node, copied_key, copied_value) end local metatable = getmetatable(node) diff --git a/spec/liluat_spec.lua b/spec/liluat_spec.lua index dfd0e4f..0f25fea 100644 --- a/spec/liluat_spec.lua +++ b/spec/liluat_spec.lua @@ -284,6 +284,29 @@ Hello, <world>! 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) end) describe("merge_tables", function () From ee780e1ee0eafc072b02ae2f3a8a1e3dbd16bf9d Mon Sep 17 00:00:00 2001 From: Max Bruckner Date: Tue, 10 May 2016 20:13:31 +0200 Subject: [PATCH 5/8] bfs_clone: support for tables with __metatable in their metatable --- liluat.lua | 4 ++-- spec/liluat_spec.lua | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/liluat.lua b/liluat.lua index e8a6e48..37ec439 100644 --- a/liluat.lua +++ b/liluat.lua @@ -107,14 +107,14 @@ local function bfs_clone(tab) rawset(copied_node, copied_key, copied_value) end - local metatable = getmetatable(node) + local metatable = debug.getmetatable(node) if metatable then if coloring[metatable] == nil then -- new node coloring[metatable] = {} table.insert(queue, metatable) end - setmetatable(coloring[node], coloring[metatable]) + debug.setmetatable(coloring[node], coloring[metatable]) end until #queue == 0 diff --git a/spec/liluat_spec.lua b/spec/liluat_spec.lua index 0f25fea..62b2e9c 100644 --- a/spec/liluat_spec.lua +++ b/spec/liluat_spec.lua @@ -307,6 +307,20 @@ Hello, <world>! 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 () From 6716c64ad4f8b771a8e785c1f9b948f9999ec378 Mon Sep 17 00:00:00 2001 From: Max Bruckner Date: Tue, 10 May 2016 20:44:30 +0200 Subject: [PATCH 6/8] remove clone_table --- liluat.lua | 20 ++------------------ spec/liluat_spec.lua | 21 --------------------- 2 files changed, 2 insertions(+), 39 deletions(-) diff --git a/liluat.lua b/liluat.lua index 37ec439..adf3b7e 100644 --- a/liluat.lua +++ b/liluat.lua @@ -122,22 +122,6 @@ local function bfs_clone(tab) end liluat.private.bfs_clone = bfs_clone --- recursively copy a table -local function clone_table(table) - local clone = {} - - for key, value in pairs(table) do - if type(value) == "table" then - clone[key] = clone_table(value) - else - clone[key] = value - end - end - - return clone -end -liluat.private.clone_table = clone_table - -- 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 @@ -145,14 +129,14 @@ local function merge_tables(a, b, shallow) a = a or {} b = b or {} - local merged = clone_table(a) + local merged = bfs_clone(a) 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) else - merged[key] = clone_table(value) + merged[key] = bfs_clone(value) end else merged[key] = value diff --git a/spec/liluat_spec.lua b/spec/liluat_spec.lua index 62b2e9c..e35d98b 100644 --- a/spec/liluat_spec.lua +++ b/spec/liluat_spec.lua @@ -169,27 +169,6 @@ Hello, <world>! end) end) - describe("clone_table", function () - it("should clone a table", function () - local table = { - a = { - b = 1, - c = { - d = 2 - } - }, - e = 3 - } - - local clone = liluat.private.clone_table(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) - end) - describe("bfs_clone", function () it("should clone a table", function () local table = { From 09e04b54a39de38c5744bdc0106d24e05a5a03eb Mon Sep 17 00:00:00 2001 From: Max Bruckner Date: Tue, 10 May 2016 21:57:46 +0200 Subject: [PATCH 7/8] First step on the way to merging cyclic tables --- liluat.lua | 46 +++++++++++++++++++++++++++++--------------- spec/liluat_spec.lua | 19 ++++++++++++++++++ 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/liluat.lua b/liluat.lua index adf3b7e..44f2113 100644 --- a/liluat.lua +++ b/liluat.lua @@ -62,15 +62,16 @@ local function escape_pattern(text) end liluat.private.escape_pattern = escape_pattern +-- replace pairs with a version that doesn't respect __pairs metamethods +local function pairs(table) + return next, table +end + -- make a deep copy of a table using breadth first search. -- supports cycles -local function bfs_clone(tab) +local function bfs_clone(tab, coloring) local queue = {} - local coloring = {} - - local function pairs(table) - return next, table - end + local coloring = coloring or {} -- color the table coloring[tab] = {} @@ -125,25 +126,38 @@ 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 = bfs_clone(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] = bfs_clone(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 e35d98b..20f0b4e 100644 --- a/spec/liluat_spec.lua +++ b/spec/liluat_spec.lua @@ -409,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 () From 12c6b9acc44a49ffed3cf879306e18651db62c14 Mon Sep 17 00:00:00 2001 From: Max Bruckner Date: Wed, 11 May 2016 11:26:03 +0200 Subject: [PATCH 8/8] bfs_clone: store parents of each node --- liluat.lua | 55 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/liluat.lua b/liluat.lua index 44f2113..21c4d47 100644 --- a/liluat.lua +++ b/liluat.lua @@ -69,57 +69,82 @@ end -- make a deep copy of a table using breadth first search. -- supports cycles -local function bfs_clone(tab, coloring) +local function bfs_clone(tab, coloring, parents) local queue = {} local coloring = coloring or {} -- color the table - coloring[tab] = {} + 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 - if coloring[key] == nil then -- new node + copied_key = coloring[key] + if copied_key == nil then -- new node if type(key) == "table" then - coloring[key] = {} + copied_key = {} table.insert(queue, key) else - coloring[key] = key + 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 - if coloring[value] == nil then -- new node + copied_value = coloring[value] + if copied_value == nil then -- new node if type(value) == "table" then - coloring[value] = {} + copied_value = {} table.insert(queue, value) else - coloring[value] = value + 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 - local copied_key = coloring[key] - local copied_value = coloring[value] - local copied_node = coloring[node] rawset(copied_node, copied_key, copied_value) end local metatable = debug.getmetatable(node) if metatable then - if coloring[metatable] == nil then -- new node - coloring[metatable] = {} + 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(coloring[node], coloring[metatable]) + debug.setmetatable(copied_node, copied_metatable) end until #queue == 0 - return coloring[tab] + return copied_tab end liluat.private.bfs_clone = bfs_clone