Module:table/deepCopy

From Linguifex
Jump to navigation Jump to search

Documentation for this module may be created at Module:table/deepCopy/doc

local debug_track_module = "Module:debug/track"

local dump = mw.dumpObject
local error = error
local getmetatable = getmetatable
local next = next
local pairs = pairs
local rawget = rawget
local type = type
local setmetatable = setmetatable

local function debug_track(...)
	debug_track = require(debug_track_module)
	return debug_track(...)
end

local tracked

local function make_copy(orig, seen, mt_flag, keep_loaded_data)
	local mt, iter, state, init = getmetatable(orig)
	-- If `mt` is nil, just use `next`, as it's faster.
	if mt == nil then
		iter, state, init = next, orig, nil
	-- `mt` could be a non-table if `__metatable` has been used, but discard it in such cases.
	elseif type(mt) ~= "table" then
		mt, iter, state, init = nil, pairs(orig)
	-- Data loaded via `mw.loadData`, which sets the key "mw_loadData" to true in the metatable.
	elseif rawget(mt, "mw_loadData") == true then
		if keep_loaded_data then
			seen[orig] = orig
			return orig
		-- Track instances of such data being copied, which is very inefficient and usually unnecessary.
		elseif not tracked then
			debug_track("table/deepCopy/loaded data")
			tracked = true
		end
		-- Discard the metatable and use the `__pairs` metamethod.
		mt, iter, state, init = nil, pairs(orig)
	-- If `mt_flag` is "none", discard the metatable and use the `__pairs` metamethod.
	elseif mt_flag == "none" then
		mt, iter, state, init = nil, pairs(orig)	
	-- Otherwise, keep `mt` and use `next` to copy the raw contents.
	else
		iter, state, init = next, orig, nil
	end
	local copy = {}
	seen[orig] = copy
	for k, v in iter, state, init do
		if k and type(k) == "table" then
			k = seen[k] or make_copy(k, seen, mt_flag, keep_loaded_data)
		end
		if v and type(v) == "table" then
			v = seen[v] or make_copy(v, seen, mt_flag, keep_loaded_data)
		end
		copy[k] = v
	end
	if mt == nil or mt_flag == "none" then
		return copy
	-- Copy the metatable if `mt_flag` is "copy"; otherwise, it will be "keep", so keep it.
	elseif mt_flag == "copy" then
		mt = seen[mt] or make_copy(mt, seen, mt_flag, keep_loaded_data)
	end
	return setmetatable(copy, mt)
end

--[==[
Recursive deep copy function. Preserves copied identities of subtables.
A more powerful version of {mw.clone}, with customizable options.
* `metatableFlag` can be one of three options:
*# "none" (the default): `pairs` will be used to copy the table, meaning that any `__pairs` metamethod will be used to copy the table, if available; the resulting table will not have a metatable.
*# "copy": a raw copy of the table will be made (i.e. any `__pairs` metamethod will be ignored), and the copy will be given a copy of the original table's metatable; this ensures that nothing from the original is retained, but may cause metamethods to behave unexpectedly, depending on their implementation.
*# "keep": a raw copy of the table will be made (i.e. any `__pairs` metamethod will be ignored), and the copy will be given the original table's metatable; this is useful when copying objects that inherit methods from a prototype object (e.g. language objects).
* If `keepLoadedData` is true, then any data loaded via {mw.loadData} will not be copied, and the original will be used instead. This is useful in iterative contexts where it is necessary to copy data being destructively modified, because objects loaded via mw.loadData are immutable.
* Notes:
*# Protected metatables will not be copied (i.e. those hidden behind a `__metatable` metamethod), as they are not accessible by Lua's design. Instead, the output of the `__metatable` method will be used instead.
*# Data loaded via {mw.loadData} is always treated as though the "none" flag is set, because the way it has been implemented causes errors to be thrown if "copy" or "keep" are used with it.]==]
return function(orig, metatableFlag, keepLoadedData)
	if metatableFlag == nil then
		metatableFlag = "none"
	elseif not (metatableFlag == "keep" or metatableFlag == "copy" or metatableFlag == "none") then
		error('metatableFlag must be "none", "copy", "keep" or nil; received ' .. dump(metatableFlag))
	end
	if orig and type(orig) == "table" then
		return make_copy(orig, {}, metatableFlag, keepLoadedData)
	end
	return orig
end